aboutsummaryrefslogtreecommitdiffstats
path: root/Zotlabs/Lib
diff options
context:
space:
mode:
Diffstat (limited to 'Zotlabs/Lib')
-rw-r--r--Zotlabs/Lib/Activity.php783
-rw-r--r--Zotlabs/Lib/ActivityStreams.php2
-rw-r--r--Zotlabs/Lib/Apps.php2
-rw-r--r--Zotlabs/Lib/BaseObject.php80
-rw-r--r--Zotlabs/Lib/Chatroom.php2
-rw-r--r--Zotlabs/Lib/Connect.php8
-rw-r--r--Zotlabs/Lib/DB_Upgrade.php31
-rw-r--r--Zotlabs/Lib/DReport.php10
-rw-r--r--Zotlabs/Lib/Enotify.php83
-rw-r--r--Zotlabs/Lib/IConfig.php6
-rw-r--r--Zotlabs/Lib/JcsEddsa2022.php25
-rw-r--r--Zotlabs/Lib/JcsEddsa2022SignException.php15
-rw-r--r--Zotlabs/Lib/Libsync.php2
-rw-r--r--Zotlabs/Lib/Libzot.php170
-rw-r--r--Zotlabs/Lib/Mailer.php86
-rw-r--r--Zotlabs/Lib/MessageFilter.php154
-rw-r--r--Zotlabs/Lib/PermissionDescription.php14
-rw-r--r--Zotlabs/Lib/SuperCurl.php127
-rw-r--r--Zotlabs/Lib/Text.php9
-rw-r--r--Zotlabs/Lib/ThreadItem.php245
-rw-r--r--Zotlabs/Lib/ThreadStream.php9
-rw-r--r--Zotlabs/Lib/Traits/HelpHelperTrait.php47
22 files changed, 1139 insertions, 771 deletions
diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php
index ae43a43b5..296129ea2 100644
--- a/Zotlabs/Lib/Activity.php
+++ b/Zotlabs/Lib/Activity.php
@@ -8,10 +8,12 @@ use Zotlabs\Access\PermissionRoles;
use Zotlabs\Access\Permissions;
use Zotlabs\Daemon\Master;
use Zotlabs\Web\HTTPSig;
+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 {
@@ -67,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;
@@ -164,7 +166,7 @@ class Activity {
}
else {
logger('fetch failed: ' . $url);
- logger($x['body']);
+ logger(print_r($x, true), LOGGER_DEBUG);
}
@@ -503,15 +505,21 @@ class Activity {
$ret['diaspora:guid'] = $i['uuid'];
$images = [];
+ $audios = [];
+ $videos = [];
+
$has_images = preg_match_all('/\[[zi]mg(.*?)](.*?)\[/ism', $i['body'], $images, PREG_SET_ORDER);
+ $has_audios = preg_match_all('/\[zaudio](.*?)\[/ism', $i['body'], $audios, PREG_SET_ORDER);
+ $has_videos = preg_match_all('/\[zvideo](.*?)\[/ism', $i['body'], $videos, PREG_SET_ORDER);
// provide ocap access token for private media.
// set this for descendants even if the current item is not private
// because it may have been relayed from a private item.
$token = IConfig::Get($i, 'ocap', 'relay');
+ $matches_processed = [];
+
if ($token && $has_images) {
- $matches_processed = [];
for ($n = 0; $n < count($images); $n++) {
$match = $images[$n];
if (str_starts_with($match[1], '=http') && str_contains($match[1], z_root() . '/photo/') && !in_array($match[1], $matches_processed)) {
@@ -526,6 +534,28 @@ class Activity {
}
}
+ if ($token && $has_audios) {
+ for ($n = 0; $n < count($audios); $n++) {
+ $match = $audios[$n];
+ if (str_contains($match[1], z_root() . '/attach/') && !in_array($match[1], $matches_processed)) {
+ $i['body'] = str_replace($match[1], $match[1] . '?token=' . $token, $i['body']);
+ $audios[$n][1] = $match[1] . '?token=' . $token;
+ $matches_processed[] = $match[1];
+ }
+ }
+ }
+
+ if ($token && $has_videos) {
+ for ($n = 0; $n < count($videos); $n++) {
+ $match = $videos[$n];
+ if (str_contains($match[1], z_root() . '/attach/') && !in_array($match[1], $matches_processed)) {
+ $i['body'] = str_replace($match[1], $match[1] . '?token=' . $token, $i['body']);
+ $videos[$n][1] = $match[1] . '?token=' . $token;
+ $matches_processed[] = $match[1];
+ }
+ }
+ }
+
if ($i['title'])
$ret['name'] = unescape_tags($i['title']);
@@ -551,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'] .= ' ';
@@ -570,6 +598,26 @@ class Activity {
if ($i['mid'] !== $i['parent_mid']) {
$ret['inReplyTo'] = ((strpos($i['thr_parent'], 'http') === 0) ? $i['thr_parent'] : z_root() . '/item/' . urlencode($i['thr_parent']));
+
+ $cnv = IConfig::Get($i['parent'], 'activitypub', 'context');
+ if (!$cnv) {
+ $cnv = $i['parent_mid'];
+ }
+ }
+
+ if (empty($cnv)) {
+ $cnv = IConfig::Get($i, 'activitypub', 'context');
+ if (!$cnv) {
+ $cnv = $i['parent_mid'];
+ }
+ }
+
+ 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;
}
if ($i['mimetype'] === 'text/bbcode') {
@@ -589,6 +637,12 @@ class Activity {
$t = self::encode_taxonomy($i);
if ($t) {
+ foreach($t as $tag) {
+ if (strcasecmp($tag['name'], '#nsfw') === 0 || strcasecmp($tag['name'], '#sensitive') === 0) {
+ $ret['sensitive'] = true;
+ }
+ }
+
$ret['tag'] = $t;
}
@@ -596,7 +650,20 @@ class Activity {
if ($a) {
$ret['attachment'] = $a;
}
-
+/*
+ if ($i['target']) {
+ if (is_string($i['target'])) {
+ $tmp = json_decode($i['target'], true);
+ if ($tmp !== null) {
+ $i['target'] = $tmp;
+ }
+ }
+ $tgt = self::encode_object($i['target']);
+ if ($tgt) {
+ $ret['target'] = $tgt;
+ }
+ }
+*/
if (intval($i['item_private']) === 0) {
$ret['to'] = [ACTIVITY_PUBLIC_INBOX];
}
@@ -627,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'])];
@@ -642,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:
@@ -694,6 +761,8 @@ class Activity {
$ret = [];
+ $token = IConfig::Get($item, 'ocap', 'relay');
+
if (!$iconfig && array_key_exists('attach', $item)) {
$atts = ((is_array($item['attach'])) ? $item['attach'] : json_decode($item['attach'], true));
if ($atts) {
@@ -702,11 +771,17 @@ class Activity {
continue;
}
- if (isset($att['type']) && strpos($att['type'], 'image')) {
- $ret[] = ['type' => 'Image', 'mediaType' => $att['type'], 'name' => $att['title'], 'url' => $att['href']];
+ if (str_starts_with($att['type'], 'image')) {
+ $ret[] = ['type' => 'Image', 'mediaType' => $att['type'], 'name' => $att['title'], 'url' => $att['href'] . (($token) ? '?token=' . $token : '')];
+ }
+ elseif (str_starts_with($att['type'], 'audio')) {
+ $ret[] = ['type' => 'Audio', 'mediaType' => $att['type'], 'name' => $att['title'], 'url' => $att['href'] . (($token) ? '?token=' . $token : '')];
+ }
+ elseif (str_starts_with($att['type'], 'video')) {
+ $ret[] = ['type' => 'Video', 'mediaType' => $att['type'], 'name' => $att['title'], 'url' => $att['href'] . (($token) ? '?token=' . $token : '')];
}
else {
- $ret[] = ['type' => 'Link', 'mediaType' => $att['type'], 'name' => $att['title'], 'href' => $att['href']];
+ $ret[] = ['type' => 'Link', 'mediaType' => $att['type'], 'name' => $att['title'], 'href' => $att['href'] . (($token) ? '?token=' . $token : '')];
}
}
}
@@ -727,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];
@@ -777,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);
@@ -807,12 +884,32 @@ class Activity {
$ret['type'] = self::activity_mapper($i['verb']);
if ((isset($i['item_deleted']) && intval($i['item_deleted'])) && !$recurse) {
- $is_response = false;
- if (ActivityStreams::is_response_activity($ret['type'])) {
+ if ($i['verb'] === 'Add' && str_contains($i['tgt_type'], 'Collection')) {
+ $ret['id'] = str_replace('/item/', '/activity/', $i['mid']) . '#Remove';
+ $ret['type'] = 'Remove';
+ if (is_string($i['obj'])) {
+ $obj = json_decode($i['obj'], true);
+ }
+ elseif(is_array($i['obj'])) {
+ $obj = $i['obj'];
+ }
+ if (isset($obj['id'])) {
+ $ret['object'] = $obj['id'];
+ }
+ else {
+ $ret['object'] = str_replace('/item/', '/activity/', $i['mid']);
+ }
+ $ret['target'] = is_array($i['target']) ? $i['target'] : json_decode($i['target'], true);
+
+ return $ret;
+ }
+
+ $is_response = ActivityStreams::is_response_activity($ret['type']);
+
+ if ($is_response) {
$ret['type'] = 'Undo';
$fragment = 'undo';
- $is_response = true;
}
else {
$ret['type'] = 'Delete';
@@ -846,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) {
@@ -890,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'
@@ -914,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'])) {
@@ -935,9 +1004,29 @@ class Activity {
// inReplyTo needs to be set in the activity for followup actions (Like, Dislike, Announce, etc.),
// but *not* for comments and RSVPs, where it should only be present in the object
- if (!in_array($ret['type'], ['Create', 'Update', 'Accept', 'Reject', 'TentativeAccept', 'TentativeReject'])) {
+ if (!in_array($ret['type'], ['Create', 'Update', 'Add', 'Remove', 'Accept', 'Reject', 'TentativeAccept', 'TentativeReject'])) {
$ret['inReplyTo'] = ((strpos($i['thr_parent'], 'http') === 0) ? $i['thr_parent'] : z_root() . '/item/' . urlencode($i['thr_parent']));
}
+
+ $cnv = IConfig::Get($i['parent'], 'activitypub', 'context');
+ if (!$cnv) {
+ $cnv = $i['parent_mid'];
+ }
+ }
+
+ if (empty($cnv)) {
+ $cnv = IConfig::Get($i, 'activitypub', 'context');
+ if (!$cnv) {
+ $cnv = $i['parent_mid'];
+ }
+ }
+
+ 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;
}
$actor = self::encode_person($i['author'], false);
@@ -946,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);
}
@@ -974,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);
}
@@ -985,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) {
@@ -1001,7 +1088,6 @@ class Activity {
$ret['to'] = [ACTIVITY_PUBLIC_INBOX];
}
-
$hookinfo = [
'item' => $i,
'encoded' => $ret
@@ -1010,6 +1096,7 @@ class Activity {
call_hooks('encode_activity', $hookinfo);
return $hookinfo['encoded'];
+
}
// Returns an array of URLS for any mention tags found in the item array $i.
@@ -1253,81 +1340,6 @@ class Activity {
// return false;
}
- static function activity_decode_mapper($verb) {
-
- $acts = [
- 'http://activitystrea.ms/schema/1.0/post' => 'Create',
- // 'http://activitystrea.ms/schema/1.0/share' => 'Announce',
- 'http://activitystrea.ms/schema/1.0/update' => 'Update',
- 'http://activitystrea.ms/schema/1.0/like' => 'Like',
- 'http://activitystrea.ms/schema/1.0/favorite' => 'Like',
- 'http://purl.org/zot/activity/dislike' => 'Dislike',
- // 'http://activitystrea.ms/schema/1.0/tag' => 'Add',
- 'http://activitystrea.ms/schema/1.0/follow' => 'Follow',
- 'http://activitystrea.ms/schema/1.0/unfollow' => 'Unfollow',
- 'http://activitystrea.ms/schema/1.0/stop-following' => 'Unfollow',
- 'http://purl.org/zot/activity/attendyes' => 'Accept',
- 'http://purl.org/zot/activity/attendno' => 'Reject',
- 'http://purl.org/zot/activity/attendmaybe' => 'TentativeAccept',
- 'Announce' => 'Announce',
- 'Invite' => 'Invite',
- 'Delete' => 'Delete',
- 'Undo' => 'Undo',
- 'Add' => 'Add',
- 'Remove' => 'Remove'
- ];
-
- call_hooks('activity_decode_mapper', $acts);
-
- foreach ($acts as $k => $v) {
- if ($verb === $v) {
- return $k;
- }
- }
-
- logger('Unmapped activity: ' . $verb);
- return 'Create';
-
- }
-
- static function activity_obj_decode_mapper($obj) {
-
- $objs = [
- 'http://activitystrea.ms/schema/1.0/note' => 'Note',
- 'http://activitystrea.ms/schema/1.0/note' => 'Article',
- 'http://activitystrea.ms/schema/1.0/comment' => 'Note',
- 'http://activitystrea.ms/schema/1.0/person' => 'Person',
- 'http://purl.org/zot/activity/profile' => 'Profile',
- 'http://activitystrea.ms/schema/1.0/photo' => 'Image',
- 'http://activitystrea.ms/schema/1.0/profile-photo' => 'Icon',
- 'http://activitystrea.ms/schema/1.0/event' => 'Event',
- 'http://purl.org/zot/activity/location' => 'Place',
- 'http://purl.org/zot/activity/chessgame' => 'Game',
- 'http://purl.org/zot/activity/tagterm' => 'zot:Tag',
- 'http://purl.org/zot/activity/thing' => 'Object',
- 'http://purl.org/zot/activity/file' => 'zot:File',
- 'http://purl.org/zot/activity/mood' => 'zot:Mood',
- 'Invite' => 'Invite',
- 'Question' => 'Question',
- 'Document' => 'Document',
- 'Audio' => 'Audio',
- 'Video' => 'Video',
- 'Delete' => 'Delete',
- 'Undo' => 'Undo'
- ];
-
- call_hooks('activity_obj_decode_mapper', $objs);
-
- foreach ($objs as $k => $v) {
- if ($obj === $v) {
- return $k;
- }
- }
-
- logger('Unmapped activity object: ' . $obj);
- return 'Note';
- }
-
static function activity_obj_mapper($obj) {
$objs = [
@@ -1606,9 +1618,9 @@ class Activity {
}
if (in_array($observer, [$r[0]['author_xchan'], $r[0]['owner_xchan']])) {
- drop_item($r[0]['id'], false, (($r[0]['item_wall']) ? DROPITEM_PHASE1 : DROPITEM_NORMAL));
+ drop_item($r[0]['id'], (($r[0]['item_wall']) ? DROPITEM_PHASE1 : DROPITEM_NORMAL), observer_hash: $observer);
} elseif (in_array($act->actor['id'], [$r[0]['author_xchan'], $r[0]['owner_xchan']])) {
- drop_item($r[0]['id'], false, (($r[0]['item_wall']) ? DROPITEM_PHASE1 : DROPITEM_NORMAL));
+ drop_item($r[0]['id'], (($r[0]['item_wall']) ? DROPITEM_PHASE1 : DROPITEM_NORMAL));
}
sync_an_item($channel['channel_id'], $r[0]['id']);
@@ -1676,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');
@@ -1687,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) {
@@ -1795,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
@@ -1906,129 +1916,173 @@ class Activity {
}
- static function update_poll($item_id, $post) {
+ static function update_poll($pollItem, $response) {
- $multi = false;
- $mid = $post['mid'];
- $content = $post['title'];
+ logger('updating poll');
- if (!$item_id) {
+ $multi = false;
+
+ if (!$pollItem) {
+ logger('no item');
return false;
}
- if (intval($post['item_blocked']) === ITEM_MODERATED) {
+ if (intval($pollItem['item_blocked']) === ITEM_MODERATED) {
+ logger('item blocked');
+ return false;
+ }
+
+ $channel = channelx_by_n($pollItem['uid']);
+ if (!$channel) {
+ logger('no channel');
return false;
}
+ $relatedItem = find_related($pollItem);
+
+ $ids = (($relatedItem) ? $pollItem['id'] . ',' . $relatedItem['id'] : $pollItem['id']);
+
dbq("START TRANSACTION");
+ // Using the provided items as is will produce desastrous race conditions
+ // in case of multiple choice polls - hence:
- $item = q("SELECT * FROM item WHERE id = %d FOR UPDATE",
- intval($item_id)
- );
+ $items = dbq("SELECT * FROM item WHERE id in ($ids) FOR UPDATE");
- if (!$item) {
- dbq("COMMIT");
- return false;
+ foreach ($items as $item) {
+ if ($item['id'] === $pollItem['id']) {
+ $pollItem = $item;
+ }
+ if (!empty($relatedItem['id']) && $item['id'] === $relatedItem['id']) {
+ $relatedItem = $item;
+ }
}
- $item = $item[0];
+ $o = json_decode($pollItem['obj'], true);
- $o = json_decode($item['obj'], true);
if ($o && array_key_exists('anyOf', $o)) {
$multi = true;
}
- $r = q("select mid, title from item where parent_mid = '%s' and author_xchan = '%s'",
- dbesc($item['mid']),
- dbesc($post['author_xchan'])
- );
+ if ($response) {
+ $mid = $response['mid'];
+ $content = trim($response['title']);
- // prevent any duplicate votes by same author for oneOf and duplicate votes with same author and same answer for anyOf
+ $r = q("select mid, title from item where parent_mid = '%s' and author_xchan = '%s' and mid != parent_mid ",
+ dbesc($pollItem['mid']),
+ dbesc($response['author_xchan'])
+ );
- if ($r) {
- if ($multi) {
- foreach ($r as $rv) {
- if ($rv['title'] === $content && $rv['mid'] !== $mid) {
- return false;
+ // prevent any duplicate votes by same author for oneOf and duplicate votes with same author and same answer for anyOf
+
+ if ($r) {
+ if ($multi) {
+ foreach ($r as $rv) {
+ if (trim($rv['title']) === $content && $rv['mid'] !== $mid) {
+ logger('already voted multi');
+ return false;
+ }
}
- }
- }
- else {
- foreach ($r as $rv) {
- if ($rv['mid'] !== $mid) {
- return false;
+ } else {
+ foreach ($r as $rv) {
+ if ($rv['mid'] !== $mid && $content) {
+ logger('already voted');
+ return false;
+ }
}
}
}
- }
- $answer_found = false;
- $found = false;
- if ($multi) {
- for ($c = 0; $c < count($o['anyOf']); $c++) {
- if ($o['anyOf'][$c]['name'] === $content) {
- $answer_found = true;
- if (is_array($o['anyOf'][$c]['replies'])) {
- foreach ($o['anyOf'][$c]['replies'] as $reply) {
- if (is_array($reply) && array_key_exists('id', $reply) && $reply['id'] === $mid) {
- $found = true;
+ $answer_found = false;
+ $foundPrevious = false;
+ if ($multi) {
+ for ($c = 0; $c < count($o['anyOf']); $c++) {
+ if (trim($o['anyOf'][$c]['name']) === $content) {
+ $answer_found = true;
+
+
+ if (is_array($o['anyOf'][$c]['replies'])) {
+ foreach ($o['anyOf'][$c]['replies'] as $reply) {
+ if (is_array($reply) && array_key_exists('id', $reply) && $reply['id'] === $mid) {
+ $foundPrevious = true;
+ }
}
}
- }
- if (!$found) {
- $o['anyOf'][$c]['replies']['totalItems']++;
- $o['anyOf'][$c]['replies']['items'][] = ['id' => $mid, 'type' => 'Note'];
+ if (!$foundPrevious) {
+ $o['anyOf'][$c]['replies']['totalItems']++;
+ $o['anyOf'][$c]['replies']['items'][] = ['id' => $mid, 'type' => 'Note'];
+ }
}
}
- }
- }
- else {
- for ($c = 0; $c < count($o['oneOf']); $c++) {
- if ($o['oneOf'][$c]['name'] === $content) {
- $answer_found = true;
- if (is_array($o['oneOf'][$c]['replies'])) {
- foreach ($o['oneOf'][$c]['replies'] as $reply) {
- if (is_array($reply) && array_key_exists('id', $reply) && $reply['id'] === $mid) {
- $found = true;
+ } else {
+ for ($c = 0; $c < count($o['oneOf']); $c++) {
+ if (trim($o['oneOf'][$c]['name']) === $content) {
+ $answer_found = true;
+ if (is_array($o['oneOf'][$c]['replies'])) {
+ foreach ($o['oneOf'][$c]['replies'] as $reply) {
+ if (is_array($reply) && array_key_exists('id', $reply) && $reply['id'] === $mid) {
+ $foundPrevious = true;
+ }
}
}
- }
- if (!$found) {
- $o['oneOf'][$c]['replies']['totalItems']++;
- $o['oneOf'][$c]['replies']['items'][] = ['id' => $mid, 'type' => 'Note'];
+ if (!$foundPrevious) {
+ $o['oneOf'][$c]['replies']['totalItems']++;
+ $o['oneOf'][$c]['replies']['items'][] = ['id' => $mid, 'type' => 'Note'];
+ }
}
}
}
}
- logger('updated_poll: ' . print_r($o, true), LOGGER_DATA);
- if ($answer_found && !$found) {
- $u = q("update item set obj = '%s', edited = '%s' where id = %d",
- dbesc(json_encode($o)),
- dbesc(datetime_convert()),
- intval($item['id'])
- );
+ if ($pollItem['comments_closed'] > NULL_DATE) {
+ if ($pollItem['comments_closed'] > datetime_convert()) {
+ $o['closed'] = datetime_convert('UTC', 'UTC', $pollItem['comments_closed'], ATOM_TIME);
+ // set this to force an update
+ $answer_found = true;
+ }
+ }
- if ($u) {
- dbq("COMMIT");
+ // A change was made locally
+ if ($response && $answer_found && !$foundPrevious) {
- if ($multi) {
- // wait some seconds for possible multiple answers to be processed
- // before calling the notifier
- sleep(3);
- }
+ // update this copy
+ $i = [$pollItem];
+ xchan_query($i, true);
+ $i = fetch_post_tags($i);
+ $i[0]['obj'] = $o;
- Master::Summon(['Notifier', 'wall-new', $item['id']]);
- return true;
- }
+ $edited = datetime_convert();
+ $i[0]['edited'] = $edited;
+
+ // create the new object
+ $newObj = self::build_packet(self::encode_activity($i[0]), $channel, true);
- dbq("ROLLBACK");
+ // and immediately update the db
+ $u = q("UPDATE item
+ SET obj = (
+ CASE
+ WHEN item.id = %d THEN '%s'
+ WHEN item.id = %d THEN '%s'
+ END
+ ),
+ edited = '%s'
+ WHERE id IN ($ids)",
+ intval($pollItem['id']),
+ dbesc(json_encode($o)),
+ intval($relatedItem['id']),
+ dbesc($newObj),
+ dbesc($edited)
+ );
+
+ dbq("COMMIT");
+ Master::Summon(['Notifier', 'edit_post', $pollItem['id'], $response['mid']]);
+ if (!empty($relatedItem['id'])) {
+ Master::Summon(['Notifier', 'edit_post', $relatedItem['id'], $response['mid']]);
+ }
}
- dbq("COMMIT");
- return false;
+ return true;
}
static function decode_note($act) {
@@ -2064,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)) {
@@ -2116,6 +2160,13 @@ class Activity {
$s['expires'] = datetime_convert('UTC', 'UTC', $act->obj['expires']);
}
+ if ($act->objprop('location')) {
+ $s['location'] = ((isset($act->objprop('location')['name'])) ? html2plain(purify_html($act->objprop('location')['name'])) : '');
+ if (isset($act->objprop('location')['latitude'], $act->objprop('location')['longitude'])) {
+ $s['coord'] = floatval($act->objprop('location')['latitude']) . ' ' . floatval($act->objprop('location')['longitude']);
+ }
+ }
+
if (in_array($act->type, ['Invite', 'Create']) && $act->objprop('type') === 'Event') {
$s['mid'] = $s['parent_mid'] = $act->id;
}
@@ -2124,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'])) {
@@ -2151,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;
}
@@ -2189,12 +2225,8 @@ class Activity {
$content['content'] = sprintf(t('&#x1f501; 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'));
@@ -2218,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')) {
@@ -2225,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;
}
}
@@ -2242,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']);
@@ -2277,6 +2316,16 @@ class Activity {
$s['obj']['actor'] = $s['obj']['actor']['id'];
}
+ if (is_array($act->tgt) && $act->tgt) {
+ if (array_key_exists('type', $act->tgt)) {
+ $s['tgt_type'] = self::activity_obj_mapper($act->tgt['type']);
+ }
+ // We shouldn't need to store collection contents which could be large. We will often only require the meta-data
+ if (isset($s['tgt_type']) && str_contains($s['tgt_type'], 'Collection')) {
+ $s['target'] = ['id' => $act->tgt['id'], 'type' => $s['tgt_type'], 'attributedTo' => $act->tgt['attributedTo'] ?? $act->tgt['actor']];
+ }
+ }
+
$generator = $act->get_property_obj('generator');
if ((!$generator) && (!$response_activity)) {
$generator = $act->get_property_obj('generator', $act->obj);
@@ -2367,7 +2416,8 @@ class Activity {
}
}
- $tag = (($poster) ? '[video poster=&quot;' . $poster . '&quot;]' : '[video]' );
+ $tag = (($poster) ? '[video poster=\'' . $poster . '\']' : '[video]' );
+
$ptr = null;
if ($act->objprop('url')) {
@@ -2582,6 +2632,7 @@ class Activity {
}
}
+
if (!$ap_rawmsg && array_key_exists('signed', $raw_arr)) {
// zap
$ap_rawmsg = json_encode($act->data, JSON_UNESCAPED_SLASHES);
@@ -2615,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;
@@ -2647,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) {
@@ -2673,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'];
@@ -2860,6 +2921,13 @@ 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['contextHistory'])) {
+ IConfig::Set($item, 'activitypub', 'context', $act->obj['contextHistory'], 1);
+ }
+ elseif (!empty($act->obj['context'])) {
+ IConfig::Set($item, 'activitypub', 'context', $act->obj['context'], 1);
+ }
+
IConfig::Set($item, 'activitypub', 'recips', $act->raw_recips);
if (intval($act->sigok)) {
@@ -2891,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'] = '';
}
@@ -2908,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) {
@@ -2922,6 +2990,7 @@ class Activity {
}
}
}
+*/
// TODO: not implemented
// self::rewrite_mentions($item);
@@ -2930,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'])) {
@@ -2967,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']);
- }
-
}
/**
@@ -3278,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;
@@ -3464,7 +3536,7 @@ class Activity {
$ret[$collection] = $actor_record[$collection];
}
}
- if (array_path_exists('endpoints/sharedInbox', $actor_record) && $actor_record['endpoints']['sharedInbox']) {
+ if (!empty($actor_record['endpoints']['sharedInbox'])) {
$ret['sharedInbox'] = $actor_record['endpoints']['sharedInbox'];
}
@@ -3567,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/',
@@ -3590,7 +3664,6 @@ class Activity {
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
'Hashtag' => 'as:Hashtag'
-
];
}
@@ -3669,5 +3742,111 @@ class Activity {
}
}
+ public static function addToCollection($channel, $object, $target, $sourceItem = null, $deliver = true) {
+ if (!isset($channel['xchan_hash'])) {
+ $channel = channelx_by_hash($channel['channel_hash']);
+ }
+
+ $item = ((new Item())
+ ->setUid($channel['channel_id'])
+ ->setVerb('Add')
+ ->setAuthorXchan($channel['channel_hash'])
+ ->setOwnerXchan($channel['channel_hash'])
+ ->setObj($object)
+ ->setObjType($object['type'])
+ ->setParentMid(str_replace('/conversation/','/item/', $target))
+ ->setThrParent(str_replace('/conversation/','/item/', $target))
+ ->setTgtType('Collection')
+ ->setTarget([
+ 'id' => str_replace('/item/','/conversation/', $target),
+ 'type' => 'Collection',
+ 'attributedTo' => z_root() . '/channel/' . $channel['channel_address'],
+ ])
+ );
+ if ($sourceItem) {
+ $item->setSourceXchan($sourceItem['source_xchan'])
+ ->setAllowCid($sourceItem['allow_cid'])
+ ->setAllowGid($sourceItem['allow_gid'])
+ ->setDenyCid($sourceItem['deny_cid'])
+ ->setDenyGid($sourceItem['deny_gid'])
+ ->setPrivate($sourceItem['item_private'])
+ ->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;
+ }
+
+ public static function removeFromCollection($channel, $object, $target, $deliver = true) {
+ if (!isset($channel['xchan_hash'])) {
+ $channel = channelx_by_hash($channel['channel_hash']);
+ }
+
+ $item = ((new Item())
+ ->setUid($channel['channel_id'])
+ ->setVerb('Remove')
+ ->setAuthorXchan($channel['channel_hash'])
+ ->setOwnerXchan($channel['channel_hash'])
+ ->setObj($object)
+ ->setObjType($object['type'])
+ ->setParentMid(str_replace('/conversation/','/item/', $target))
+ ->setThrParent(str_replace('/conversation/','/item/', $target))
+ ->setReplyto(z_root() . '/channel/' . $channel['channel_address'])
+ ->setTgtType('Collection')
+ ->setTarget([
+ 'id' => str_replace('/item/','/conversation/', $target),
+ 'type' => 'Collection',
+ 'attributedTo' => z_root() . '/channel/' . $channel['channel_address']
+ ])
+ );
+
+ $result = post_activity_item($item->toArray(), deliver: $deliver, channel: $channel, observer: $channel, addAndSync: false);
+ logger('removeFromCollection: ' . print_r($result, true));
+ 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/BaseObject.php b/Zotlabs/Lib/BaseObject.php
new file mode 100644
index 000000000..7125d34cb
--- /dev/null
+++ b/Zotlabs/Lib/BaseObject.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Zotlabs\Lib;
+
+use Zotlabs\ActivityStreams\UnhandledElementException;
+
+class BaseObject
+{
+
+ public $string;
+ public $ldContext;
+
+ /**
+ * @param $input
+ * @param $strict
+ * @throws UnhandledElementException if $strict
+ */
+
+ public function __construct($input = null, $strict = false)
+ {
+ if (isset($input)) {
+ if (is_string($input)) {
+ $this->string = $input;
+ }
+ elseif(is_array($input)) {
+ foreach ($input as $key => $value) {
+ $key = ($key === '@context') ? 'ldContext' : $key;
+ if ($strict && !property_exists($this, $key)) {
+ throw new UnhandledElementException("Unhandled element: $key");
+ }
+ $this->{$key} = $value;
+ }
+ }
+ }
+ return $this;
+ }
+
+ public function getDataType($element, $object = null)
+ {
+ $object = $object ?? $this;
+ $type = gettype($object[$element]);
+ if ($type === 'array' && array_is_list($object[$element])) {
+ return 'list';
+ }
+ return $type;
+ }
+
+ public function toArray()
+ {
+ if ($this->string) {
+ return $this->string;
+ }
+ $returnValue = [];
+ foreach ((array) $this as $key => $value) {
+ if (isset($value)) {
+ $key = ($key === 'ldContext') ? '@context' : $key;
+ $returnValue[$key] = (($value instanceof BaseObject) ? $value->toArray() : $value);
+ }
+ }
+ return $returnValue;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getLdContext()
+ {
+ return $this->ldContext;
+ }
+
+ /**
+ * @param mixed $ldContext
+ * @return BaseObject
+ */
+ public function setLdContext($ldContext)
+ {
+ $this->ldContext = $ldContext;
+ return $this;
+ }
+}
diff --git a/Zotlabs/Lib/Chatroom.php b/Zotlabs/Lib/Chatroom.php
index 34853b6ab..ee16d0002 100644
--- a/Zotlabs/Lib/Chatroom.php
+++ b/Zotlabs/Lib/Chatroom.php
@@ -181,7 +181,7 @@ class Chatroom {
}
- function leave($observer_xchan, $room_id, $client) {
+ public static function leave($observer_xchan, $room_id, $client) {
if(! $room_id || ! $observer_xchan)
return;
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/DB_Upgrade.php b/Zotlabs/Lib/DB_Upgrade.php
index 981c354a4..e11c2eb10 100644
--- a/Zotlabs/Lib/DB_Upgrade.php
+++ b/Zotlabs/Lib/DB_Upgrade.php
@@ -1,18 +1,35 @@
<?php
+/**
+ * A class to handle database schema upgrades.
+ *
+ * SPDX-FileCopyrightText: 2024 Hubzilla Community
+ * SPDX-FileContributor: Harald Eilertsen
+ *
+ * SPDX-License-Identifier: MIT
+ */
namespace Zotlabs\Lib;
use Zotlabs\Lib\Config;
+/**
+ * Upgrade the database schema if necessary.
+ *
+ * Compares the currently active database schema version with the version
+ * required for this version of Hubzilla, and performs the upgrade if needed.
+ *
+ * If the difference consists of more than one revision of the schema, each of
+ * the intermediate upgrades are performed in turn.
+ */
class DB_Upgrade {
- public $config_name = '';
- public $func_prefix = '';
-
- function __construct($db_revision) {
-
- $this->config_name = 'db_version';
- $this->func_prefix = '_';
+ /**
+ * Check the installed and required schema versions and perform the upgrade
+ * if necessary.
+ *
+ * @param int $db_version The required DB schema version.
+ */
+ public static function run(int $db_revision): void {
$build = Config::Get('system', 'db_version', 0);
if(! intval($build))
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 121ad9b09..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;
@@ -406,6 +417,7 @@ class Enotify {
}
elseif (isset($params['type']) && $params['type'] === NOTIFY_TAGSHARE) {
+ $itemlink = $params['link'];
$subject = sprintf( t('[$Projectname:Notify] %s tagged your post') , $sender['xchan_name']);
$preamble = sprintf( t('%1$s tagged your post at %2$s'),$sender['xchan_name'], $sitename);
$epreamble = sprintf( t('%1$s tagged [zrl=%2$s]your post[/zrl]') ,
@@ -415,7 +427,6 @@ class Enotify {
$sitelink = t('Please visit %s to view and/or reply to the conversation.');
$tsitelink = sprintf( $sitelink, $siteurl );
$hsitelink = sprintf( $sitelink, '<a href="' . $siteurl . '">' . $sitename . '</a>');
- $itemlink = $params['link'];
}
elseif (isset($params['type']) && $params['type'] === NOTIFY_INTRO) {
@@ -433,6 +444,7 @@ class Enotify {
}
elseif (isset($params['type']) && $params['type'] === NOTIFY_SUGGEST) {
+ $itemlink = $params['link'];
$subject = sprintf( t('[$Projectname:Notify] Friend suggestion received'));
$preamble = sprintf( t('You\'ve received a friend suggestion from \'%1$s\' at %2$s'), $sender['xchan_name'], $sitename);
$epreamble = sprintf( t('You\'ve received [zrl=%1$s]a friend suggestion[/zrl] for %2$s from %3$s.'),
@@ -447,7 +459,6 @@ class Enotify {
$sitelink = t('Please visit %s to approve or reject the suggestion.');
$tsitelink = sprintf( $sitelink, $siteurl );
$hsitelink = sprintf( $sitelink, '<a href="' . $siteurl . '">' . $sitename . '</a>');
- $itemlink = $params['link'];
}
elseif (isset($params['type']) && $params['type'] === NOTIFY_CONFIRM) {
@@ -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'])
);
@@ -595,7 +612,7 @@ class Enotify {
// send email notification if notification preferences permit
- require_once('bbcode.php');
+ require_once('include/bbcode.php');
if ((intval($recip['channel_notifyflags']) & intval($params['type'])) || $params['type'] == NOTIFY_SYSTEM) {
logger('notification: sending notification email');
@@ -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/Libsync.php b/Zotlabs/Lib/Libsync.php
index a7e33ba6b..c6b149738 100644
--- a/Zotlabs/Lib/Libsync.php
+++ b/Zotlabs/Lib/Libsync.php
@@ -885,7 +885,7 @@ class Libsync {
dbesc($t)
);
- q("update hubloc set hubloc_error = 1, hubloc_deleted = 1 where hubloc_url = '%s' and hubloc_sitekey != '%s'",
+ q("update hubloc set hubloc_error = 1, hubloc_deleted = 1 where hubloc_url = '%s' and hubloc_sitekey != '%s' and hubloc_network = 'zot6'",
dbesc($r[0]['hubloc_url']),
dbesc($r[0]['hubloc_sitekey'])
);
diff --git a/Zotlabs/Lib/Libzot.php b/Zotlabs/Lib/Libzot.php
index bc944c97c..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
@@ -1134,6 +1139,7 @@ class Libzot {
}
$message_request = false;
+ $is_collection_operation = false;
$has_data = array_key_exists('data', $env) && $env['data'];
@@ -1141,15 +1147,33 @@ class Libzot {
$AS = null;
+
if ($env['encoding'] === 'activitystreams') {
$AS = new ActivityStreams($data);
- if (!$AS->is_valid()) {
- logger('Activity rejected: ' . print_r($data, true));
- return;
+
+ // process add/remove from collection separately, as it requires a target.
+ // 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)
+ && array_key_exists('actor', $AS->obj)
+ && !empty($AS->tgt)) {
+
+ logger('relayed collection operation', LOGGER_DEBUG);
+ $is_collection_operation = true;
+
+ $original_id = $AS->id;
+ $original_type = $AS->type;
+
+ $raw_activity = $AS->data;
+
+ $AS = new ActivityStreams($raw_activity['object'], portable_id: $env['sender']);
}
+
if (is_array($AS->obj)) {
$item = Activity::decode_note($AS);
+
if (!$item) {
logger('Could not decode activity: ' . print_r($AS, true));
return;
@@ -1158,6 +1182,12 @@ class Libzot {
else {
$item = [];
}
+
+ if (!$AS->is_valid()) {
+ logger('Activity rejected: ' . print_r($data, true));
+ return;
+ }
+
logger($AS->debug(), LOGGER_DATA);
}
@@ -1198,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;
@@ -1265,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;
}
@@ -1301,7 +1311,7 @@ class Libzot {
$relay = (($env['type'] === 'response') ? true : false);
- $result = self::process_delivery($env['sender'], $AS, $item, $deliveries, $relay, false, $message_request);
+ $result = self::process_delivery($env['sender'], $AS, $item, $deliveries, $relay, false, $message_request, false, $is_collection_operation);
Activity::init_background_fetch($env['sender']);
}
@@ -1517,7 +1527,7 @@ class Libzot {
* @return array
*/
- static function process_delivery($sender, $act, $arr, $deliveries, $relay, $public = false, $request = false, $force = false) {
+ static function process_delivery($sender, $act, $arr, $deliveries, $relay, $public = false, $request = false, $force = false, $is_collection_operation = false) {
$result = [];
// We've validated the sender. Now make sure that the sender is the owner or author
@@ -1532,6 +1542,7 @@ class Libzot {
$local_public = $public;
$item_result = null;
+ $parent = null;
$DR = new DReport(z_root(), $sender, $d, $arr['mid'], $arr['uuid']);
@@ -1545,6 +1556,14 @@ class Libzot {
$DR->set_name($channel['channel_name'] . ' <' . channel_reddress($channel) . '>');
+ $conversation_operation = $is_collection_operation && isset($arr['target']['attributedTo']);
+
+ if (isset($arr['tgt_type']) && str_contains($arr['tgt_type'], 'Collection') && !$relay && !$conversation_operation) {
+ $DR->update('not a collection activity');
+ $result[] = $DR->get();
+ continue;
+ }
+
if (($act) && ($act->obj) && (!is_array($act->obj))) {
// The initial object fetch failed using the sys channel credentials.
// Try again using the delivery channel credentials.
@@ -1578,6 +1597,8 @@ class Libzot {
*
*/
+
+
if ($sender === $channel['channel_hash'] && $arr['author_xchan'] === $channel['channel_hash'] && !str_starts_with($arr['mid'], z_root())) {
$DR->update('self delivery ignored');
$result[] = $DR->get();
@@ -1624,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);
@@ -1701,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;
}
}
@@ -1826,11 +1850,12 @@ class Libzot {
dbesc($arr['author_xchan'])
);
- // reactions such as like and dislike could have an mid with /activity/ in it.
+ // 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",
+
+ $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'])),
+ dbesc(reverse_activity_mid($arr['mid'])),
intval($channel['channel_id'])
);
@@ -1859,21 +1884,29 @@ class Libzot {
$DR->update('update ignored');
$result[] = $DR->get();
}
+
+ if ($relay && $channel['channel_hash'] === $item_result['item']['owner_xchan'] && $item_result['item']['verb'] !== 'Add' && !$is_collection_operation) {
+ $approval = Activity::addToCollection($channel, $act->data, $item_result['item']['parent_mid'], $item_result['item'], deliver: false);
+ }
+
}
else {
$DR->update('update ignored');
$result[] = $DR->get();
- // We need this line to ensure wall-to-wall comments are relayed (by falling through to the relay bit),
+ // We need this line to ensure wall-to-wall comments and add/remove activities are relayed (by falling through to the relay bit),
// and at the same time not relay any other relayable posts more than once, because to do so is very wasteful.
if (!intval($r[0]['item_origin']))
continue;
}
+
+
}
else {
$arr['aid'] = $channel['channel_account_id'];
$arr['uid'] = $channel['channel_id'];
+
// if it's a sourced post, call the post_local hooks as if it were
// posted locally so that crosspost connectors will be triggered.
$item_source = check_item_source($arr['uid'], $arr);
@@ -1902,10 +1935,15 @@ class Libzot {
}
if (post_is_importable($arr['uid'], $arr, $abook)) {
- $item_result = item_store($arr);
+ $item_result = item_store($arr, addAndSync: false);
+
if ($item_result['success']) {
$item_id = $item_result['item_id'];
+ if ($relay && $channel['channel_hash'] === $item_result['item']['owner_xchan'] && $item_result['item']['verb'] !== 'Add' && !$is_collection_operation) {
+ $approval = Activity::addToCollection($channel, $act->data, $item_result['item']['parent_mid'], $item_result['item'], deliver: false);
+ }
+
if ($item_source && in_array($item_result['item']['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) {
event_addtocal($item_id, $channel['channel_id']);
}
@@ -1948,14 +1986,18 @@ 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']) {
+ Master::Summon(['Notifier', 'relay', intval($approval['item_id'])]);
+ }
+
$DR->addto_update('relayed');
- $result[] = $DR->get();
+ $result = [$DR->get()];
}
}
@@ -2006,6 +2048,7 @@ class Libzot {
foreach ($items as $activity) {
$AS = new ActivityStreams($activity);
+
if ($AS->is_valid() && $AS->type === 'Announce' && is_array($AS->obj)
&& array_key_exists('object', $AS->obj) && array_key_exists('actor', $AS->obj)) {
// This is a relayed/forwarded Activity (as opposed to a shared/boosted object)
@@ -2014,6 +2057,30 @@ class Libzot {
$AS = new ActivityStreams($AS->obj);
}
+ // process add/remove from collection separately, as it requires a target.
+ // use the raw 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)
+ && array_key_exists('actor', $AS->obj)
+ && !empty($AS->tgt)) {
+
+ logger('relayed collection operation', LOGGER_DEBUG);
+
+ $is_collection_operation = true;
+
+ $original_id = $AS->id;
+ $original_type = $AS->type;
+
+ $raw_activity = $AS->data;
+
+ $AS = new ActivityStreams($raw_activity['object']);
+
+ // Store the original activity id and type for later usage
+ $AS->meta['original_id'] = $original_id;
+ $AS->meta['original_type'] = $original_type;
+ }
+
if (!$AS->is_valid()) {
logger('Fetched activity rejected: ' . print_r($activity, true));
continue;
@@ -2207,7 +2274,7 @@ class Libzot {
}
- $x = item_store_update($item);
+ $x = item_store_update($item, addAndSync: false);
// If we're updating an event that we've saved locally, we store the item info first
// because event_addtocal will parse the body to get the 'new' event details
@@ -2323,21 +2390,20 @@ class Libzot {
);
}
} 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'])
- );
- }
- }
+ 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
// and then clean up after ourselves with a cron job after several days to do the delete_item_lowlevel() (DROPITEM_PHASE2).
- drop_item($post_id, false, DROPITEM_PHASE1);
+ drop_item($post_id, DROPITEM_PHASE1, uid: $uid);
tag_deliver($uid, $post_id);
}
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/PermissionDescription.php b/Zotlabs/Lib/PermissionDescription.php
index 51d5f890d..13df60b60 100644
--- a/Zotlabs/Lib/PermissionDescription.php
+++ b/Zotlabs/Lib/PermissionDescription.php
@@ -117,7 +117,7 @@ class PermissionDescription {
}
/**
- * Returns an icon css class name if an appropriate one is available, e.g. "fa-globe" for Public,
+ * Returns an icon css class name if an appropriate one is available, e.g. "bi-globe" for Public,
* otherwise returns empty string.
*
* @return string icon css class name (often FontAwesome)
@@ -125,12 +125,12 @@ class PermissionDescription {
public function get_permission_icon() {
switch($this->channel_perm) {
- case 0:/* only me */ return 'fa-eye-slash';
- case PERMS_PUBLIC: return 'fa-globe';
- case PERMS_NETWORK: return 'fa-share-alt-square'; // fa-share-alt-square is very similiar to the hubzilla logo, but we should create our own logo class to use
- case PERMS_SITE: return 'fa-sitemap';
- case PERMS_CONTACTS: return 'fa-group';
- case PERMS_SPECIFIC: return 'fa-list';
+ case 0:/* only me */ return 'bi-eye-slash';
+ case PERMS_PUBLIC: return 'bi-globe';
+ case PERMS_NETWORK: return 'bi-share'; // bi-share is very similiar to the hubzilla logo, but we should create our own logo class to use
+ case PERMS_SITE: return 'bi-geo';
+ case PERMS_CONTACTS: return 'bi-people';
+ case PERMS_SPECIFIC: return 'bi-list-ul';
case PERMS_AUTHED: return '';
case PERMS_PENDING: return '';
default: return '';
diff --git a/Zotlabs/Lib/SuperCurl.php b/Zotlabs/Lib/SuperCurl.php
deleted file mode 100644
index 462a62b36..000000000
--- a/Zotlabs/Lib/SuperCurl.php
+++ /dev/null
@@ -1,127 +0,0 @@
-<?php
-
-namespace Zotlabs\Lib;
-
-/**
- * @brief wrapper for z_fetch_url() which can be instantiated with several built-in parameters and
- * these can be modified and re-used. Useful for CalDAV and other processes which need to authenticate
- * and set lots of CURL options (many of which stay the same from one call to the next).
- */
-
-
-
-
-class SuperCurl {
-
-
- private $auth;
- private $url;
-
- private $curlopt = array();
-
- private $headers = null;
- public $filepos = 0;
- public $filehandle = 0;
- public $request_data = '';
-
- private $request_method = 'GET';
- private $upload = false;
- private $cookies = false;
-
-
- private function set_data($s) {
- $this->request_data = $s;
- $this->filepos = 0;
- }
-
- public function curl_read($ch,$fh,$size) {
-
- if($this->filepos < 0) {
- unset($fh);
- return '';
- }
-
- $s = substr($this->request_data,$this->filepos,$size);
-
- if(strlen($s) < $size)
- $this->filepos = (-1);
- else
- $this->filepos = $this->filepos + $size;
-
- return $s;
- }
-
-
- public function __construct($opts = array()) {
- $this->set($opts);
- }
-
- private function set($opts = array()) {
- if($opts) {
- foreach($opts as $k => $v) {
- switch($k) {
- case 'http_auth':
- $this->auth = $v;
- break;
- case 'magicauth':
- // currently experimental
- $this->magicauth = $v;
- \Zotlabs\Daemon\Master::Summon([ 'CurlAuth', $v ]);
- break;
- case 'custom':
- $this->request_method = $v;
- break;
- case 'url':
- $this->url = $v;
- break;
- case 'data':
- $this->set_data($v);
- if($v) {
- $this->upload = true;
- }
- else {
- $this->upload = false;
- }
- break;
- case 'headers':
- $this->headers = $v;
- break;
- default:
- $this->curlopts[$k] = $v;
- break;
- }
- }
- }
- }
-
- function exec() {
- $opts = $this->curlopts;
- $url = $this->url;
- if($this->auth)
- $opts['http_auth'] = $this->auth;
- if($this->magicauth) {
- $opts['cookiejar'] = 'store/[data]/cookie_' . $this->magicauth;
- $opts['cookiefile'] = 'store/[data]/cookie_' . $this->magicauth;
- $opts['cookie'] = 'PHPSESSID=' . trim(file_get_contents('store/[data]/cookien_' . $this->magicauth));
- $c = channelx_by_n($this->magicauth);
- if($c)
- $url = zid($this->url,channel_reddress($c));
- }
- if($this->custom)
- $opts['custom'] = $this->custom;
- if($this->headers)
- $opts['headers'] = $this->headers;
- if($this->upload) {
- $opts['upload'] = true;
- $opts['infile'] = $this->filehandle;
- $opts['infilesize'] = strlen($this->request_data);
- $opts['readfunc'] = [ $this, 'curl_read' ] ;
- }
-
- $recurse = 0;
- return z_fetch_url($this->url,true,$recurse,(($opts) ? $opts : null));
-
- }
-
-
-}
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 1082bf642..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();
@@ -93,7 +88,7 @@ class ThreadItem {
$buttons = '';
$dropping = false;
$star = false;
- $isstarred = "unstarred fa-star-o";
+ $isstarred = "unstarred bi-star";
$is_comment = false;
$is_item = false;
$osparkle = '';
@@ -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
@@ -123,13 +120,13 @@ class ThreadItem {
$locktype = 0;
}
- $shareable = ((($conv->get_profile_owner() == local_channel() && local_channel()) && (intval($item['item_private']) === 0)) ? true : false);
+ $shareable = ((local_channel() && $conv->get_profile_owner() == local_channel()) && (intval($item['item_private']) === 0));
// allow an exemption for sharing stuff from your private feeds
if($item['author']['xchan_network'] === 'rss')
$shareable = true;
- $repeatable = ((($conv->get_profile_owner() == local_channel() && local_channel()) && (intval($item['item_private']) === 0) && (in_array($item['author']['xchan_network'], ['zot6', 'activitypub']))) ? true : false);
+ $repeatable = ((local_channel() && $conv->get_profile_owner() == local_channel()) && intval($item['item_private']) === 0 && in_array($item['author']['xchan_network'], ['zot6', 'activitypub']));
// @fixme
// Have recently added code to properly handle polls in group reshares by redirecting all of the poll responses to the group.
@@ -187,7 +184,7 @@ class ThreadItem {
$drop = [ 'dropping' => true, 'delete' => t('Admin Delete') ];
}
- $filer = ((($conv->get_profile_owner() == local_channel()) && (! array_key_exists('real_uid',$item))) ? t("Save to Folder") : false);
+ $filer = (((local_channel() && $conv->get_profile_owner() === local_channel()) || (local_channel() && App::$module === 'pubstream')) ? t("Save to Folder") : false);
$profile_avatar = $item['author']['xchan_photo_s'];
$profile_link = chanlink_hash($item['author_xchan']);
@@ -204,72 +201,26 @@ class ThreadItem {
$response_verbs[] = 'dislike';
}
- $response_verbs[] = 'announce';
+ if ($repeatable) {
+ $response_verbs[] = 'announce';
+ }
- if(in_array($item['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) {
- $response_verbs[] = 'attendyes';
- $response_verbs[] = 'attendno';
- $response_verbs[] = 'attendmaybe';
+ if (in_array($item['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) {
+ $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'));
}
}
- if($item['obj_type'] === 'Question') {
+ if ($item['obj_type'] === 'Question') {
$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
@@ -279,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"),
@@ -291,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') : '');
@@ -324,14 +280,12 @@ 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 on this comment"), t("reply"), t("Reply to"));
+ $reply_to = array( t("Reply to this message"), t("reply"), t("Reply to"));
+ $reactions_allowed = true;
}
$share = [];
@@ -374,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);
@@ -386,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
@@ -407,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'],
@@ -420,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,
@@ -446,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,
@@ -468,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(),
@@ -475,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 : ''),
@@ -496,49 +456,50 @@ 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'),
'moderate_delete' => t('Delete'),
'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'),
+ '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);
@@ -547,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;
-// 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;
+ $visible_comments = 3; // Config::Get('system', 'expanded_comments', 3);
- 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'] = sprintf( t('%s show all'), '<i class="fa fa-chevron-down"></i>');
- 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;
}
}
@@ -826,7 +773,7 @@ class ThreadItem {
*/
private function get_comment_box() {
- if(!$this->is_toplevel() && !Config::Get('system','thread_allow')) {
+ if(!$this->is_toplevel()) {
return '';
}
@@ -862,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'),
@@ -890,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 = '';
@@ -904,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;
@@ -924,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 b7711bbd5..69b3cc21b 100644
--- a/Zotlabs/Lib/Traits/HelpHelperTrait.php
+++ b/Zotlabs/Lib/Traits/HelpHelperTrait.php
@@ -15,6 +15,15 @@ trait HelpHelperTrait {
private string $file_type = '';
/**
+ * Associative array containing the detected language.
+ */
+ private array $lang = [
+ 'language' => 'en', //! Detected language, 2-letter ISO 639-1 code ("en")
+ 'from_url' => false, //! true if language from URL overrides browser default
+ 'missing' => false, //! true if topic not found in detected language
+ ];
+
+ /**
* Determines help language.
*
* If the language was specified in the URL, override the language preference
@@ -28,17 +37,15 @@ trait HelpHelperTrait {
$languages = $language_repository->getList();
if(array_key_exists(argv(1), $languages)) {
- $lang = argv(1);
- $from_url = true;
+ $this->lang['language'] = argv(1);
+ $this->lang['from_url'] = true;
} else {
- $lang = \App::$language;
- if(! isset($lang))
- $lang = 'en';
+ if(isset(\App::$language)) {
+ $this->lang['language'] = \App::$language;
+ }
- $from_url = false;
+ $this->lang['from_url'] = false;
}
-
- $this->lang = array('language' => $lang, 'from_url' => $from_url);
}
/**
@@ -53,10 +60,10 @@ trait HelpHelperTrait {
// Use local variable until we can use trait constants.
$valid_file_ext = ['md', 'bb', 'html'];
- $base_path = "doc/{$lang}/${base_path}";
+ $base_path_with_lang = "doc/{$lang}/${base_path}";
foreach ($valid_file_ext as $ext) {
- $path = "{$base_path}.{$ext}";
+ $path = "{$base_path_with_lang}.{$ext}";
if (file_exists($path)) {
$this->file_name = $path;
$this->file_type = $ext;
@@ -64,5 +71,25 @@ trait HelpHelperTrait {
break;
}
}
+
+ if (empty($this->file_name) && $lang !== 'en') {
+ $this->lang['missing'] = true;
+ $this->find_help_file($base_path, 'en');
+ }
+ }
+
+ public function missing_translation(): bool {
+ return !!$this->lang['missing'];
+ }
+
+ public function missing_translation_message(): string {
+ $prefered_language_name = get_language_name(
+ $this->lang['language'],
+ $this->lang['language']
+ );
+
+ return bbcode(
+ 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.")
+ );
}
}