diff options
Diffstat (limited to 'Zotlabs/Lib')
-rw-r--r-- | Zotlabs/Lib/Activity.php | 614 | ||||
-rw-r--r-- | Zotlabs/Lib/Apps.php | 9 | ||||
-rw-r--r-- | Zotlabs/Lib/BaseObject.php | 80 | ||||
-rw-r--r-- | Zotlabs/Lib/Cache.php | 9 | ||||
-rw-r--r-- | Zotlabs/Lib/Chatroom.php | 2 | ||||
-rw-r--r-- | Zotlabs/Lib/Config.php | 2 | ||||
-rw-r--r-- | Zotlabs/Lib/Connect.php | 13 | ||||
-rw-r--r-- | Zotlabs/Lib/Crypto.php | 3 | ||||
-rw-r--r-- | Zotlabs/Lib/DB_Upgrade.php | 62 | ||||
-rw-r--r-- | Zotlabs/Lib/DReport.php | 4 | ||||
-rw-r--r-- | Zotlabs/Lib/Enotify.php | 45 | ||||
-rw-r--r-- | Zotlabs/Lib/Libsync.php | 9 | ||||
-rw-r--r-- | Zotlabs/Lib/Libzot.php | 215 | ||||
-rw-r--r-- | Zotlabs/Lib/Libzotdir.php | 27 | ||||
-rw-r--r-- | Zotlabs/Lib/Mailer.php | 86 | ||||
-rw-r--r-- | Zotlabs/Lib/PermissionDescription.php | 14 | ||||
-rw-r--r-- | Zotlabs/Lib/QueueWorker.php | 65 | ||||
-rw-r--r-- | Zotlabs/Lib/SuperCurl.php | 127 | ||||
-rw-r--r-- | Zotlabs/Lib/ThreadItem.php | 144 | ||||
-rw-r--r-- | Zotlabs/Lib/Traits/HelpHelperTrait.php | 95 | ||||
-rw-r--r-- | Zotlabs/Lib/XConfig.php | 2 | ||||
-rw-r--r-- | Zotlabs/Lib/Zotfinger.php | 3 |
22 files changed, 1003 insertions, 627 deletions
diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php index b628221fb..1f7d4be0c 100644 --- a/Zotlabs/Lib/Activity.php +++ b/Zotlabs/Lib/Activity.php @@ -8,6 +8,7 @@ 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'); @@ -70,7 +71,7 @@ class Activity { } if ($items) { - return self::encode_item(array_shift($items), true); + return self::encode_item(array_shift($items)); } return null; @@ -503,15 +504,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 +533,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']); @@ -570,6 +599,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 +638,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 +651,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]; } @@ -694,6 +762,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 +772,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 +803,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]; @@ -807,12 +883,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'; @@ -935,9 +1031,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); @@ -1010,6 +1126,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 +1370,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 +1648,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 +1718,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 +1729,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 +1835,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 @@ -1875,7 +1915,7 @@ class Activity { } if ($icon) { - Master::Summon(['Xchan_photo', bin2hex($icon), bin2hex($url)]); + Master::Summon(['Xchan_photo', bin2hex($icon), bin2hex($url), $force]); } } @@ -1906,129 +1946,173 @@ class Activity { } - static function update_poll($item_id, $post) { + static function update_poll($pollItem, $response) { + + logger('updating poll'); + + $multi = false; - $multi = false; - $mid = $post['mid']; - $content = $post['title']; + if (!$pollItem) { + logger('no item'); + return false; + } - if (!$item_id) { + if (intval($pollItem['item_blocked']) === ITEM_MODERATED) { + logger('item blocked'); return false; } - if (intval($post['item_blocked']) === ITEM_MODERATED) { + $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; - dbq("ROLLBACK"); + // create the new object + $newObj = self::build_packet(self::encode_activity($i[0]), $channel, true); + // 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) { @@ -2116,6 +2200,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; } @@ -2218,6 +2309,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 +2318,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; } } @@ -2277,6 +2370,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); @@ -2582,6 +2685,7 @@ class Activity { } } + if (!$ap_rawmsg && array_key_exists('signed', $raw_arr)) { // zap $ap_rawmsg = json_encode($act->data, JSON_UNESCAPED_SLASHES); @@ -2615,8 +2719,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 +2750,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 +2778,22 @@ class Activity { return; } + $relay = $channel['channel_hash'] === $parent[0]['owner_xchan']; + + if (str_contains($parent[0]['tgt_type'], 'Collection') && !$relay && !$isCollectionOperation) { + 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 +2974,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)) { @@ -2902,7 +3023,7 @@ class Activity { if (intval($parent[0]['item_private'])) { if (!intval($item['item_private'])) { - $item['item_private'] = intval($parent_item['item_private']); + $item['item_private'] = intval($parent[0]['item_private']); $item['allow_cid'] = '<' . $channel['channel_hash'] . '>'; $item['allow_gid'] = $item['deny_cid'] = $item['deny_gid'] = ''; } @@ -2930,17 +3051,48 @@ 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' && !$isCollectionOperation) { + $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']]); + } + } + + $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']); } if ($fetch_parents && $parent && !intval($parent[0]['item_private'])) { @@ -2967,28 +3119,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']); - } - } /** @@ -3271,7 +3401,7 @@ class Activity { return $content; } - if ($act['type'] === 'Event') { + if (isset($act['type']) && $act['type'] === 'Event') { $adjust = false; $event = []; $event['event_hash'] = $act['id']; @@ -3464,7 +3594,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 +3697,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 +3722,6 @@ class Activity { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'Hashtag' => 'as:Hashtag' - ]; } @@ -3669,5 +3800,76 @@ 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; + } } diff --git a/Zotlabs/Lib/Apps.php b/Zotlabs/Lib/Apps.php index 1c05d69b1..0dc405ea9 100644 --- a/Zotlabs/Lib/Apps.php +++ b/Zotlabs/Lib/Apps.php @@ -3,6 +3,7 @@ namespace Zotlabs\Lib; use App; +use Zotlabs\Lib\Config; require_once('include/plugin.php'); require_once('include/channel.php'); @@ -65,7 +66,7 @@ class Apps { } static public function get_base_apps() { - $x = get_config('system','base_apps',[ + $x = Config::Get('system','base_apps',[ 'Connections', 'Contact Roles', 'Network', @@ -301,7 +302,7 @@ class Apps { break; default: if($config) - $unset = ((get_config('system', $require[0]) == $require[1]) ? false : true); + $unset = ((Config::Get('system', $require[0]) == $require[1]) ? false : true); else $unset = ((local_channel() && feature_enabled(local_channel(),$require)) ? false : true); if($unset) @@ -523,7 +524,7 @@ class Apps { break; default: if($config) - $unset = ((get_config('system', $require[0]) === $require[1]) ? false : true); + $unset = ((Config::Get('system', $require[0]) === $require[1]) ? false : true); else $unset = ((local_channel() && feature_enabled(local_channel(),$require)) ? false : true); if($unset) @@ -960,7 +961,7 @@ class Apps { $conf = (($menu === 'nav_featured_app') ? 'app_order' : 'app_pin_order'); - $x = (($uid) ? get_pconfig($uid,'system',$conf) : get_config('system',$conf)); + $x = (($uid) ? get_pconfig($uid,'system',$conf) : Config::Get('system',$conf)); if(($x) && (! is_array($x))) { $y = explode(',',$x); $y = array_map('trim',$y); 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/Cache.php b/Zotlabs/Lib/Cache.php index f3f520496..4b5beb2aa 100644 --- a/Zotlabs/Lib/Cache.php +++ b/Zotlabs/Lib/Cache.php @@ -2,10 +2,11 @@ namespace Zotlabs\Lib; - /** - * cache api - */ +use Zotlabs\Lib\Config; +/** + * cache api + */ class Cache { /** @@ -23,7 +24,7 @@ class Cache { $r = q("SELECT v FROM cache WHERE k = '%s' AND updated > %s - INTERVAL %s LIMIT 1", dbesc($hash), db_utcnow(), - db_quoteinterval(($age ? $age : get_config('system','object_cache_days', '30') . ' DAY')) + db_quoteinterval(($age ? $age : Config::Get('system','object_cache_days', '30') . ' DAY')) ); if ($r) 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/Config.php b/Zotlabs/Lib/Config.php index 95df8ed6f..cd8b08991 100644 --- a/Zotlabs/Lib/Config.php +++ b/Zotlabs/Lib/Config.php @@ -115,7 +115,7 @@ class Config { * The category of the configuration value * @param string $key * The configuration key to query - * @param string $default (optional) default false + * @param mixed $default (optional) default false * @return mixed Return value or false on error or if not set */ public static function Get($family, $key, $default = false) { diff --git a/Zotlabs/Lib/Connect.php b/Zotlabs/Lib/Connect.php index 4de41526b..9f6d077b4 100644 --- a/Zotlabs/Lib/Connect.php +++ b/Zotlabs/Lib/Connect.php @@ -5,8 +5,7 @@ namespace Zotlabs\Lib; use App; use Zotlabs\Access\Permissions; use Zotlabs\Daemon\Master; - - +use Zotlabs\Lib\Config; class Connect { @@ -25,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; @@ -96,7 +101,7 @@ class Connect { $wf = discover_by_webbie($url,$protocol); if (! $wf) { - $feeds = get_config('system','feed_contacts'); + $feeds = Config::Get('system','feed_contacts'); if (($feeds) && (in_array($protocol, [ '', 'feed', 'rss' ]))) { $d = discover_by_url($url); diff --git a/Zotlabs/Lib/Crypto.php b/Zotlabs/Lib/Crypto.php index 188c6bd81..46a25d3b5 100644 --- a/Zotlabs/Lib/Crypto.php +++ b/Zotlabs/Lib/Crypto.php @@ -3,6 +3,7 @@ namespace Zotlabs\Lib; use Exception; +use Zotlabs\Lib\Config; class Crypto { @@ -44,7 +45,7 @@ class Crypto { 'encrypt_key' => false ]; - $conf = get_config('system', 'openssl_conf_file'); + $conf = Config::Get('system', 'openssl_conf_file'); if ($conf) { $openssl_options['config'] = $conf; diff --git a/Zotlabs/Lib/DB_Upgrade.php b/Zotlabs/Lib/DB_Upgrade.php index b6e3f7b7b..e11c2eb10 100644 --- a/Zotlabs/Lib/DB_Upgrade.php +++ b/Zotlabs/Lib/DB_Upgrade.php @@ -1,21 +1,39 @@ <?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) { + /** + * 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 { - $this->config_name = 'db_version'; - $this->func_prefix = '_'; - - $build = get_config('system', 'db_version', 0); + $build = Config::Get('system', 'db_version', 0); if(! intval($build)) - $build = set_config('system', 'db_version', $db_revision); + $build = Config::Set('system', 'db_version', $db_revision); if($build == $db_revision) { // Nothing to be done. @@ -27,7 +45,7 @@ class DB_Upgrade { logger('Critical: check_config unable to determine database schema version'); return; } - + $current = intval($db_revision); if($stored < $current) { @@ -38,7 +56,7 @@ class DB_Upgrade { for($x = $stored + 1; $x <= $current; $x ++) { $s = '_' . $x; $cls = '\\Zotlabs\Update\\' . $s ; - if(! class_exists($cls)) { + if(! class_exists($cls)) { return; } @@ -52,10 +70,10 @@ class DB_Upgrade { Config::Load('database'); - if(get_config('database', $s)) + if(Config::Get('database', $s)) break; - set_config('database',$s, '1'); - + Config::Set('database',$s, '1'); + $c = new $cls(); @@ -65,10 +83,10 @@ class DB_Upgrade { $source = t('Source code of failed update: ') . "\n\n" . @file_get_contents('Zotlabs/Update/' . $s . '.php'); - + // Prevent sending hundreds of thousands of emails by creating - // a lockfile. + // a lockfile. $lockfile = 'store/[data]/mailsent'; @@ -77,7 +95,7 @@ class DB_Upgrade { @unlink($lockfile); //send the administrator an e-mail file_put_contents($lockfile, $x); - + $r = q("select account_language from account where account_email = '%s' limit 1", dbesc(\App::$config['system']['admin_email']) ); @@ -86,7 +104,7 @@ class DB_Upgrade { [ 'toEmail' => \App::$config['system']['admin_email'], 'messageSubject' => sprintf( t('Update Error at %s'), z_root()), - 'textVersion' => replace_macros(get_intltext_template('update_fail_eml.tpl'), + 'textVersion' => replace_macros(get_intltext_template('update_fail_eml.tpl'), [ '$sitename' => \App::$config['system']['sitename'], '$siteurl' => z_root(), @@ -104,11 +122,11 @@ class DB_Upgrade { pop_lang(); } else { - set_config('database',$s, 'success'); + Config::Set('database',$s, 'success'); } } } - set_config('system', 'db_version', $db_revision); + Config::Set('system', 'db_version', $db_revision); } } -}
\ No newline at end of file +} diff --git a/Zotlabs/Lib/DReport.php b/Zotlabs/Lib/DReport.php index 71e39a9d7..ac8e0d377 100644 --- a/Zotlabs/Lib/DReport.php +++ b/Zotlabs/Lib/DReport.php @@ -1,6 +1,8 @@ <?php namespace Zotlabs\Lib; +use Zotlabs\Lib\Config; + class DReport { private $location; @@ -70,7 +72,7 @@ class DReport { static function is_storable($dr) { - if(get_config('system', 'disable_dreport')) + if(Config::Get('system', 'disable_dreport')) return false; /** diff --git a/Zotlabs/Lib/Enotify.php b/Zotlabs/Lib/Enotify.php index 48a255e95..6820091d5 100644 --- a/Zotlabs/Lib/Enotify.php +++ b/Zotlabs/Lib/Enotify.php @@ -6,6 +6,7 @@ namespace Zotlabs\Lib; * @brief File with functions and a class for generating system and email notifications. */ +use Zotlabs\Lib\Config; class Enotify { @@ -61,7 +62,7 @@ class Enotify { $product = t('$projectname'); // PLATFORM_NAME; $siteurl = z_root(); $thanks = t('Thank You,'); - $sitename = get_config('system','sitename'); + $sitename = Config::Get('system','sitename'); $site_admin = sprintf( t('%s Administrator'), $sitename); $opt_out1 = sprintf( t('This email was sent by %1$s at %2$s.'), t('$Projectname'), \App::get_hostname()); $opt_out2 = sprintf( t('To stop receiving these messages, please adjust your Notification Settings at %s'), z_root() . '/settings'); @@ -73,15 +74,15 @@ class Enotify { // Do not translate 'noreply' as it must be a legal 7-bit email address - $reply_email = get_config('system', 'reply_address'); + $reply_email = Config::Get('system', 'reply_address'); if(! $reply_email) $reply_email = 'noreply' . '@' . $hostname; - $sender_email = get_config('system', 'from_email'); + $sender_email = Config::Get('system', 'from_email'); if(! $sender_email) $sender_email = 'Administrator' . '@' . $hostname; - $sender_name = get_config('system', 'from_email_name'); + $sender_name = Config::Get('system', 'from_email_name'); if(! $sender_name) $sender_name = \Zotlabs\Lib\System::get_site_name(); @@ -212,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. @@ -307,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,13 +328,14 @@ class Enotify { $verb = (($moderated) ? t('requested to dislike') : t('disliked')); // "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; @@ -405,6 +414,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]') , @@ -414,7 +424,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) { @@ -432,6 +441,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.'), @@ -446,7 +456,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) { @@ -594,7 +603,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'); diff --git a/Zotlabs/Lib/Libsync.php b/Zotlabs/Lib/Libsync.php index 3130290f7..c6b149738 100644 --- a/Zotlabs/Lib/Libsync.php +++ b/Zotlabs/Lib/Libsync.php @@ -5,6 +5,7 @@ namespace Zotlabs\Lib; use App; use Zotlabs\Daemon\Master; +use Zotlabs\Lib\Config; class Libsync { @@ -135,7 +136,7 @@ class Libsync { $info['collection_members'] = $r; } - $interval = get_config('queueworker', 'queue_interval', 500000); + $interval = Config::Get('queueworker', 'queue_interval', 500000); logger('Packet: ' . print_r($info, true), LOGGER_DATA, LOG_DEBUG); @@ -157,7 +158,7 @@ class Libsync { /* $x = q("select count(outq_hash) as total from outq where outq_delivered = 0"); - if (intval($x[0]['total']) > intval(get_config('system', 'force_queue_threshold', 3000))) { + if (intval($x[0]['total']) > intval(Config::Get('system', 'force_queue_threshold', 3000))) { logger('immediate delivery deferred.', LOGGER_DEBUG, LOG_INFO); Queue::update($hash); continue; @@ -266,7 +267,7 @@ class Libsync { } if ($cat !== 'hz_delpconfig') { - set_pconfig($channel['channel_id'],$cat,$k,$v,$pconfig_updated[$k]); + set_pconfig($channel['channel_id'], $cat, $k, $v, $pconfig_updated[$k]); } } } @@ -884,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 3495ede06..57c110d8b 100644 --- a/Zotlabs/Lib/Libzot.php +++ b/Zotlabs/Lib/Libzot.php @@ -3,10 +3,11 @@ namespace Zotlabs\Lib; use App; -use Zotlabs\Web\HTTPSig; -use Zotlabs\Access\Permissions; use Zotlabs\Access\PermissionLimits; +use Zotlabs\Access\Permissions; use Zotlabs\Daemon\Master; +use Zotlabs\Lib\Config; +use Zotlabs\Web\HTTPSig; require_once('include/crypto.php'); @@ -101,12 +102,12 @@ class Libzot { */ static function build_packet($channel, $type = 'activity', $recipients = null, $msg = [], $encoding = 'activitystreams', $remote_key = null, $methods = '') { - $sig_method = get_config('system', 'signature_algorithm', 'sha256'); + $sig_method = Config::Get('system', 'signature_algorithm', 'sha256'); $data = [ 'type' => $type, 'encoding' => $encoding, 'sender' => $channel['channel_hash'], - 'site_id' => self::make_xchan_hash(z_root(), get_config('system', 'pubkey')), + 'site_id' => self::make_xchan_hash(z_root(), Config::Get('system', 'pubkey')), 'version' => System::get_zot_revision(), ]; @@ -654,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 @@ -661,7 +667,7 @@ class Libzot { */ call_hooks('import_xchan', $arr); - $dirmode = intval(get_config('system', 'directory_mode', DIRECTORY_MODE_NORMAL)); + $dirmode = intval(Config::Get('system', 'directory_mode', DIRECTORY_MODE_NORMAL)); $changed = false; $what = ''; @@ -710,7 +716,7 @@ class Libzot { // if we import an entry from a site that's not ours and either or both of us is off the grid - hide the entry. /** @TODO: check if we're the same directory realm, which would mean we are allowed to see it */ - $dirmode = get_config('system', 'directory_mode'); + $dirmode = Config::Get('system', 'directory_mode'); if (((isset($arr['site']['directory_mode']) && $arr['site']['directory_mode'] === 'standalone') || ($dirmode & DIRECTORY_MODE_STANDALONE)) && ($arr['site']['url'] != z_root())) $arr['searchable'] = false; @@ -1008,7 +1014,7 @@ class Libzot { logger('Headers: ' . print_r($arr['header'], true), LOGGER_DATA, LOG_DEBUG); } - $x = Crypto::unencapsulate($x, get_config('system', 'prvkey')); + $x = Crypto::unencapsulate($x, Config::Get('system', 'prvkey')); if ($x && !is_array($x)) { $x = json_decode($x, true); @@ -1133,6 +1139,7 @@ class Libzot { } $message_request = false; + $is_collection_operation = false; $has_data = array_key_exists('data', $env) && $env['data']; @@ -1140,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; @@ -1157,6 +1182,12 @@ class Libzot { else { $item = []; } + + if (!$AS->is_valid()) { + logger('Activity rejected: ' . print_r($data, true)); + return; + } + logger($AS->debug(), LOGGER_DATA); } @@ -1197,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; @@ -1264,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; } @@ -1300,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']); } @@ -1416,7 +1427,7 @@ class Libzot { $include_sys = false; if ($msg['type'] === 'activity') { - $disable_discover_tab = get_config('system', 'disable_discover_tab') || get_config('system', 'disable_discover_tab') === false; + $disable_discover_tab = Config::Get('system', 'disable_discover_tab') || Config::Get('system', 'disable_discover_tab') === false; if (!$disable_discover_tab) $include_sys = true; @@ -1516,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 @@ -1544,6 +1555,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. @@ -1577,6 +1596,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(); @@ -1623,22 +1644,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 = get_config('system','pubstream_incl'); - $excl = get_config('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); @@ -1700,6 +1723,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; } } @@ -1825,11 +1849,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']) ); @@ -1858,21 +1883,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); @@ -1901,10 +1934,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']); } @@ -1947,12 +1985,16 @@ 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(); } @@ -2005,6 +2047,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) @@ -2013,6 +2056,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; @@ -2206,7 +2273,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 @@ -2322,21 +2389,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); } @@ -2568,9 +2634,14 @@ class Libzot { if (!$observer) return ''; - $parsed = parse_url($observer['xchan_url']); + $url = $observer['xchan_url']; + if (preg_match('|^https?://|', $url) === 0) { + $url = "https://{$url}"; + } + + $parsed = parse_url($url); - return $parsed['scheme'] . '://' . $parsed['host'] . (($parsed['port']) ? ':' . $parsed['port'] : '') . '/rpost?f='; + return $parsed['scheme'] . '://' . $parsed['host'] . (isset($parsed['port']) ? ':' . $parsed['port'] : '') . '/rpost?f='; } /** @@ -2924,8 +2995,8 @@ class Libzot { */ static function site_info() { - $signing_key = get_config('system', 'prvkey'); - $sig_method = get_config('system', 'signature_algorithm', 'sha256'); + $signing_key = Config::Get('system', 'prvkey'); + $sig_method = Config::Get('system', 'signature_algorithm', 'sha256'); $ret = []; $ret['site'] = []; @@ -2934,10 +3005,10 @@ class Libzot { $ret['site']['post'] = z_root() . '/zot'; $ret['site']['openWebAuth'] = z_root() . '/owa'; $ret['site']['authRedirect'] = z_root() . '/magic'; - $ret['site']['sitekey'] = get_config('system', 'pubkey'); + $ret['site']['sitekey'] = Config::Get('system', 'pubkey'); $ret['site']['directory_mode'] = 'normal'; - $dirmode = get_config('system', 'directory_mode'); + $dirmode = Config::Get('system', 'directory_mode'); if ($dirmode == DIRECTORY_MODE_PRIMARY) $ret['site']['directory_mode'] = 'primary'; @@ -2956,7 +3027,7 @@ class Libzot { if ($dirmode != DIRECTORY_MODE_STANDALONE) { - $register_policy = intval(get_config('system', 'register_policy')); + $register_policy = intval(Config::Get('system', 'register_policy')); if ($register_policy == REGISTER_CLOSED) $ret['site']['register_policy'] = 'closed'; @@ -2966,7 +3037,7 @@ class Libzot { $ret['site']['register_policy'] = 'open'; - $access_policy = intval(get_config('system', 'access_policy')); + $access_policy = intval(Config::Get('system', 'access_policy')); if ($access_policy == ACCESS_PRIVATE) $ret['site']['access_policy'] = 'private'; @@ -2982,7 +3053,7 @@ class Libzot { require_once('include/channel.php'); $ret['site']['channels'] = channel_total(); - $ret['site']['admin'] = get_config('system', 'admin_email'); + $ret['site']['admin'] = Config::Get('system', 'admin_email'); $visible_plugins = []; if (is_array(App::$plugins) && count(App::$plugins)) { @@ -2993,10 +3064,10 @@ class Libzot { } $ret['site']['plugins'] = $visible_plugins; - $ret['site']['sitehash'] = get_config('system', 'location_hash'); - $ret['site']['sitename'] = get_config('system', 'sitename'); - $ret['site']['sellpage'] = get_config('system', 'sellpage'); - $ret['site']['location'] = get_config('system', 'site_location'); + $ret['site']['sitehash'] = Config::Get('system', 'location_hash'); + $ret['site']['sitename'] = Config::Get('system', 'sitename'); + $ret['site']['sellpage'] = Config::Get('system', 'sellpage'); + $ret['site']['location'] = Config::Get('system', 'site_location'); $ret['site']['realm'] = get_directory_realm(); $ret['site']['project'] = System::get_platform_name(); $ret['site']['version'] = System::get_project_version(); diff --git a/Zotlabs/Lib/Libzotdir.php b/Zotlabs/Lib/Libzotdir.php index ca3902a9e..25f308fc2 100644 --- a/Zotlabs/Lib/Libzotdir.php +++ b/Zotlabs/Lib/Libzotdir.php @@ -2,6 +2,7 @@ namespace Zotlabs\Lib; +use Zotlabs\Lib\Config; use Zotlabs\Lib\Libzot; use Zotlabs\Lib\Zotfinger; use Zotlabs\Lib\Webfinger; @@ -20,7 +21,7 @@ class Libzotdir { static function find_upstream_directory($dirmode) { - $preferred = get_config('system','directory_server'); + $preferred = Config::Get('system','directory_server'); // Thwart attempts to use a private directory @@ -47,17 +48,17 @@ class Libzotdir { $directory_fallback_servers = get_directory_fallback_servers(); - $dirmode = intval(get_config('system','directory_mode')); + $dirmode = intval(Config::Get('system','directory_mode')); if ($dirmode == DIRECTORY_MODE_NORMAL) { $toss = mt_rand(0,count($directory_fallback_servers)); $preferred = $directory_fallback_servers[$toss]; if(! $preferred) { $preferred = DIRECTORY_FALLBACK_MASTER; } - set_config('system','directory_server',$preferred); + Config::Set('system','directory_server',$preferred); } else { - set_config('system','directory_server',z_root()); + Config::Set('system','directory_server',z_root()); } } if($preferred) { @@ -77,7 +78,7 @@ class Libzotdir { static function check_upstream_directory() { - $directory = get_config('system', 'directory_server'); + $directory = Config::Get('system', 'directory_server'); // it's possible there is no directory server configured and the local hub is being used. // If so, default to preserving the absence of a specific server setting. @@ -94,7 +95,7 @@ class Libzotdir { } if (! $isadir) - set_config('system', 'directory_server', ''); + Config::Set('system', 'directory_server', ''); } @@ -106,7 +107,7 @@ class Libzotdir { $ret = ((array_key_exists($setting,$_SESSION)) ? intval($_SESSION[$setting]) : false); if($ret === false) - $ret = get_config('directory', $setting); + $ret = Config::Get('directory', $setting); // 'safemode' is the default if there is no observer or no established preference. @@ -114,7 +115,7 @@ class Libzotdir { if($setting === 'safemode' && $ret === false) $ret = 1; - if($setting === 'globaldir' && intval(get_config('system','localdir_hide'))) + if($setting === 'globaldir' && intval(Config::Get('system','localdir_hide'))) $ret = 1; return $ret; @@ -133,7 +134,7 @@ class Libzotdir { $globaldir = self::get_directory_setting($observer, 'globaldir'); $pubforums = self::get_directory_setting($observer, 'pubforums'); - $hide_local = intval(get_config('system','localdir_hide')); + $hide_local = intval(Config::Get('system','localdir_hide')); if($hide_local) $globaldir = 1; @@ -141,7 +142,7 @@ class Libzotdir { // Build urls without order and pubforums so it's easy to tack on the changed value // Probably there's an easier way to do this - $directory_sort_order = get_config('system','directory_sort_order'); + $directory_sort_order = Config::Get('system','directory_sort_order'); if(! $directory_sort_order) $directory_sort_order = 'date'; @@ -232,7 +233,7 @@ class Libzotdir { if (! $r) return; - $dir_trusted_hosts = array_merge(get_directory_fallback_servers(), get_config('system', 'trusted_directory_servers', [])); + $dir_trusted_hosts = array_merge(get_directory_fallback_servers(), Config::Get('system', 'trusted_directory_servers', [])); foreach ($r as $rr) { if (! $rr['site_directory']) @@ -244,7 +245,7 @@ class Libzotdir { // It will take about a month for a new directory to obtain the full current repertoire of channels. /** @FIXME Go back and pick up earlier ratings if this is a new directory server. These do not get refreshed. */ - $token = get_config('system','realm_token'); + $token = Config::Get('system','realm_token'); $syncdate = (($rr['site_sync'] <= NULL_DATE) ? datetime_convert('UTC','UTC','now - 2 days') : $rr['site_sync']); $x = z_fetch_url($rr['site_directory'] . '?f=&sync=' . urlencode($syncdate) . (($token) ? '&t=' . $token : '')); @@ -696,7 +697,7 @@ class Libzotdir { static function update($hash, $addr, $bump_date = true, $flag = null) { - $dirmode = intval(get_config('system', 'directory_mode')); + $dirmode = intval(Config::Get('system', 'directory_mode')); if($dirmode == DIRECTORY_MODE_NORMAL) { return; 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/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/QueueWorker.php b/Zotlabs/Lib/QueueWorker.php index 68e747b0f..9e28999c9 100644 --- a/Zotlabs/Lib/QueueWorker.php +++ b/Zotlabs/Lib/QueueWorker.php @@ -4,6 +4,9 @@ namespace Zotlabs\Lib; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Exception\UnableToBuildUuidException; +use Zotlabs\Lib\Config; + +require_once 'include/dba/dba_transaction.php'; class QueueWorker { @@ -28,18 +31,6 @@ class QueueWorker { 'Expire' ]; - private static function qstart() { - q('START TRANSACTION'); - } - - private static function qcommit() { - q("COMMIT"); - } - - private static function qrollback() { - q("ROLLBACK"); - } - public static function Summon($argv) { if ($argv[0] !== 'Queueworker') { @@ -65,26 +56,26 @@ class QueueWorker { logger('queueworker_stats_summon: cmd:' . $argv[0] . ' ' . 'timestamp:' . time()); - self::qstart(); + $transaction = new \DbaTransaction(\DBA::$dba); $r = q("INSERT INTO workerq (workerq_priority, workerq_data, workerq_uuid, workerq_cmd) VALUES (%d, '%s', '%s', '%s')", intval($priority), - $workinfo_json, + dbesc($workinfo_json), dbesc($uuid), dbesc($argv[0]) ); if (!$r) { - self::qrollback(); + // Transaction is autmatically rolled back on return logger("INSERT FAILED", LOGGER_DEBUG); return; } - self::qcommit(); + $transaction->commit(); logger('INSERTED: ' . $workinfo_json, LOGGER_DEBUG); } $workers = self::GetWorkerCount(); if ($workers < self::$maxworkers) { logger($workers . '/' . self::$maxworkers . ' workers active', LOGGER_DEBUG); - $phpbin = get_config('system', 'phpbin', 'php'); + $phpbin = Config::Get('system', 'phpbin', 'php'); proc_run($phpbin, 'Zotlabs/Daemon/Master.php', ['Queueworker']); } } @@ -111,19 +102,19 @@ class QueueWorker { return; } - self::qstart(); + $transaction = new \DbaTransaction(\DBA::$dba); $r = q("INSERT INTO workerq (workerq_priority, workerq_data, workerq_uuid, workerq_cmd) VALUES (%d, '%s', '%s', '%s')", intval($priority), - $workinfo_json, + dbesc($workinfo_json), dbesc($uuid), dbesc($argv[0]) ); if (!$r) { - self::qrollback(); + // Transaction is automatically rolled back on return logger("Insert failed: " . $workinfo_json, LOGGER_DEBUG); return; } - self::qcommit(); + $transaction->commit(); logger('INSERTED: ' . $workinfo_json, LOGGER_DEBUG); } @@ -132,18 +123,18 @@ class QueueWorker { public static function GetWorkerCount() { if (self::$maxworkers == 0) { - self::$maxworkers = get_config('queueworker', 'max_queueworkers', 4); + self::$maxworkers = Config::Get('queueworker', 'max_queueworkers', 4); self::$maxworkers = self::$maxworkers > 3 ? self::$maxworkers : 4; } if (self::$workermaxage == 0) { - self::$workermaxage = get_config('queueworker', 'max_queueworker_age'); + self::$workermaxage = Config::Get('queueworker', 'max_queueworker_age'); self::$workermaxage = self::$workermaxage > 120 ? self::$workermaxage : 300; } - self::qstart(); + $transaction = new \DbaTransaction(\DBA::$dba); // skip locked is preferred but is not supported by mariadb < 10.6 which is still used a lot - hence make it optional - $sql_quirks = ((get_config('system', 'db_skip_locked_supported')) ? 'SKIP LOCKED' : 'NOWAIT'); + $sql_quirks = ((Config::Get('system', 'db_skip_locked_supported')) ? 'SKIP LOCKED' : 'NOWAIT'); $r = q("SELECT workerq_id FROM workerq WHERE workerq_reservationid IS NOT NULL AND workerq_processtimeout < %s FOR UPDATE $sql_quirks", db_utcnow() @@ -158,7 +149,7 @@ class QueueWorker { $u = dbq("update workerq set workerq_reservationid = null where workerq_id in ($ids)"); } - self::qcommit(); + $transaction->commit(); //q("update workerq set workerq_reservationid = null where workerq_reservationid is not null and workerq_processtimeout < %s", //db_utcnow() @@ -196,15 +187,15 @@ class QueueWorker { private static function getWorkId() { self::GetWorkerCount(); - self::qstart(); + $transaction = new \DbaTransaction(\DBA::$dba); // skip locked is preferred but is not supported by mariadb < 10.6 which is still used a lot - hence make it optional - $sql_quirks = ((get_config('system', 'db_skip_locked_supported')) ? 'SKIP LOCKED' : 'NOWAIT'); + $sql_quirks = ((Config::Get('system', 'db_skip_locked_supported')) ? 'SKIP LOCKED' : 'NOWAIT'); $work = dbq("SELECT workerq_id, workerq_cmd FROM workerq WHERE workerq_reservationid IS NULL ORDER BY workerq_priority DESC, workerq_id ASC LIMIT 1 FOR UPDATE $sql_quirks"); if (!$work) { - self::qrollback(); + // Transaction automatically rolled back on return return false; } @@ -224,24 +215,24 @@ class QueueWorker { ); if (!$work) { - self::qrollback(); + // Transaction automatically rolled back on return logger("Could not update workerq.", LOGGER_DEBUG); return false; } logger("GOTWORK: " . json_encode($work), LOGGER_DEBUG); - self::qcommit(); + $transaction->commit(); return $id; } public static function Process() { - $sleep = intval(get_config('queueworker', 'queue_worker_sleep', 100)); - $auto_queue_worker_sleep = get_config('queueworker', 'auto_queue_worker_sleep', 0); + $sleep = intval(Config::Get('queueworker', 'queue_worker_sleep', 100)); + $auto_queue_worker_sleep = Config::Get('queueworker', 'auto_queue_worker_sleep', 0); if (!self::GetWorkerID()) { if ($auto_queue_worker_sleep) { - set_config('queueworker', 'queue_worker_sleep', $sleep + 100); + Config::Set('queueworker', 'queue_worker_sleep', $sleep + 100); } logger('Unable to get worker ID. Exiting.', LOGGER_DEBUG); @@ -250,7 +241,7 @@ class QueueWorker { if ($auto_queue_worker_sleep && $sleep > 100) { $next_sleep = $sleep - 100; - set_config('queueworker', 'queue_worker_sleep', (($next_sleep < 100) ? 100 : $next_sleep)); + Config::Set('queueworker', 'queue_worker_sleep', (($next_sleep < 100) ? 100 : $next_sleep)); } $jobs = 0; @@ -259,7 +250,7 @@ class QueueWorker { self::$workersleep = $sleep; self::$workersleep = ((intval(self::$workersleep) > 100) ? intval(self::$workersleep) : 100); - if (function_exists('sys_getloadavg') && get_config('queueworker', 'load_average_sleep')) { + if (function_exists('sys_getloadavg') && Config::Get('queueworker', 'load_average_sleep')) { // very experimental! $load_average_sleep = true; } @@ -287,7 +278,7 @@ class QueueWorker { if ($workers < self::$maxworkers) { logger($workers . '/' . self::$maxworkers . ' workers active', LOGGER_DEBUG); - $phpbin = get_config('system', 'phpbin', 'php'); + $phpbin = Config::Get('system', 'phpbin', 'php'); proc_run($phpbin, 'Zotlabs/Daemon/Master.php', ['Queueworker']); } 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/ThreadItem.php b/Zotlabs/Lib/ThreadItem.php index 8f364e945..e0db98eb3 100644 --- a/Zotlabs/Lib/ThreadItem.php +++ b/Zotlabs/Lib/ThreadItem.php @@ -3,8 +3,9 @@ namespace Zotlabs\Lib; use App; -use Zotlabs\Lib\Apps; use Zotlabs\Access\AccessList; +use Zotlabs\Lib\Apps; +use Zotlabs\Lib\Config; require_once('include/text.php'); @@ -41,9 +42,7 @@ class ThreadItem { $this->data = $data; $this->toplevel = ($this->get_id() == $this->get_data_value('parent')); - $this->threaded = get_config('system','thread_allow'); - - $observer = \App::get_observer(); + $this->threaded = Config::Get('system','thread_allow'); // Prepare the children if(isset($data['children'])) { @@ -70,7 +69,7 @@ class ThreadItem { // allow a site to configure the order and content of the reaction emoji list if($this->toplevel) { - $x = get_config('system','reactions'); + $x = Config::Get('system','reactions'); if($x && is_array($x) && count($x)) { $this->reactions = $x; } @@ -94,7 +93,7 @@ class ThreadItem { $buttons = ''; $dropping = false; $star = false; - $isstarred = "unstarred fa-star-o"; + $isstarred = "unstarred bi-star"; $is_comment = false; $is_item = false; $osparkle = ''; @@ -124,13 +123,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. @@ -188,7 +187,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']); @@ -205,9 +204,11 @@ class ThreadItem { $response_verbs[] = 'dislike'; } - $response_verbs[] = 'announce'; + if ($repeatable) { + $response_verbs[] = 'announce'; + } - if(in_array($item['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) { + if (in_array($item['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) { $response_verbs[] = 'attendyes'; $response_verbs[] = 'attendno'; $response_verbs[] = 'attendmaybe'; @@ -217,60 +218,21 @@ class ThreadItem { } } - if($item['obj_type'] === 'Question') { + if ($item['obj_type'] === 'Question') { $response_verbs[] = 'answer'; } - if(! feature_enabled($conv->get_profile_owner(),'dislike')) + 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']) : ''); -*/ /* * We should avoid doing this all the time, but it depends on the conversation mode @@ -328,11 +290,13 @@ class ThreadItem { $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 comment"), t("reply"), t("Reply to")); + $reactions_allowed = true; } $share = []; @@ -347,12 +311,12 @@ class ThreadItem { $dreport = ''; - $keep_reports = intval(get_config('system','expire_delivery_reports')); + $keep_reports = intval(Config::Get('system','expire_delivery_reports')); if($keep_reports === 0) $keep_reports = 10; $dreport_link = ''; - if((intval($item['item_type']) == ITEM_TYPE_POST) && (! get_config('system','disable_dreport')) && strcmp(datetime_convert('UTC','UTC',$item['created']),datetime_convert('UTC','UTC',"now - $keep_reports days")) > 0) { + if((intval($item['item_type']) == ITEM_TYPE_POST) && (! Config::Get('system','disable_dreport')) && strcmp(datetime_convert('UTC','UTC',$item['created']),datetime_convert('UTC','UTC',"now - $keep_reports days")) > 0) { $dreport = t('Delivery Report'); $dreport_link = '?mid=' . $item['mid']; } @@ -363,7 +327,8 @@ class ThreadItem { localize_item($item); - $body = prepare_body($item,true); + $opts = (($item['resource_type'] === 'event') ? ['is_event_item' => true] : []); + $body = prepare_body($item, true, $opts); // $viewthread (below) is only valid in list mode. If this is a channel page, build the thread viewing link // since we can't depend on llink or plink pointing to the right local location. @@ -397,7 +362,7 @@ class ThreadItem { $json_mids = json_encode($mids); // Pinned item processing - $allowed_type = (in_array($item['item_type'], get_config('system', 'pin_types', [ ITEM_TYPE_POST ])) ? true : false); + $allowed_type = (in_array($item['item_type'], Config::Get('system', 'pin_types', [ ITEM_TYPE_POST ])) ? true : false); $pinned_items = ($allowed_type ? get_pconfig($item['uid'], 'pinned', $item['item_type'], []) : []); $pinned = ((!empty($pinned_items) && in_array($midb64, $pinned_items)) ? true : false); @@ -411,7 +376,6 @@ class ThreadItem { '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'], @@ -446,16 +410,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, @@ -475,7 +438,7 @@ 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 : ''), @@ -485,7 +448,7 @@ class ThreadItem { 'embed' => $embed, 'rawmid' => $item['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,7 +459,7 @@ 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, @@ -504,30 +467,9 @@ class ThreadItem { '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 : ''), -*/ 'modal_dismiss' => t('Close'), - // 'showlike' => $showlike, - // 'showdislike' => $showdislike, 'comment' => ($item['item_delayed'] ? '' : $this->get_comment_box()), + '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'), @@ -539,6 +481,9 @@ class ThreadItem { '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') ); $arr = array('item' => $item, 'output' => $tmp_item); @@ -549,15 +494,7 @@ class ThreadItem { $result['children'] = array(); $nb_children = count($children); - $visible_comments = get_config('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 = Config::Get('system', 'expanded_comments', 3); if(($this->get_display_mode() === 'normal') && ($nb_children > 0)) { foreach($children as $child) { @@ -567,7 +504,7 @@ class ThreadItem { if(($nb_children > $visible_comments) || ($thread_level > 1)) { $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>'); + $result['children'][0]['hide_text'] = t('show all'); if($thread_level > 1) { $result['children'][$nb_children - 1]['comment_lastcollapsed'] = true; } @@ -826,7 +763,7 @@ class ThreadItem { */ private function get_comment_box() { - if(!$this->is_toplevel() && !get_config('system','thread_allow')) { + if(!$this->is_toplevel() && !Config::Get('system','thread_allow')) { return ''; } @@ -862,6 +799,7 @@ 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'), @@ -869,12 +807,12 @@ class ThreadItem { '$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'), '$cipher' => $conv->get_cipher(), - '$sourceapp' => \App::$sourcename, + '$sourceapp' => App::$sourcename, '$observer' => get_observer_hash(), '$anoncomments' => ((in_array($conv->get_mode(), ['channel', 'display', 'cards', 'articles']) && perm_is_allowed($conv->get_profile_owner(),'','post_comments')) ? true : false), '$anonname' => [ 'anonname', t('Your full name (required)') ], @@ -890,7 +828,7 @@ 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(); diff --git a/Zotlabs/Lib/Traits/HelpHelperTrait.php b/Zotlabs/Lib/Traits/HelpHelperTrait.php new file mode 100644 index 000000000..63b0eb22e --- /dev/null +++ b/Zotlabs/Lib/Traits/HelpHelperTrait.php @@ -0,0 +1,95 @@ +<?php + +namespace Zotlabs\Lib\Traits; + +use CommerceGuys\Intl\Language\LanguageRepository; + +trait HelpHelperTrait { + + // PHP versions before 8.2 does not support trait constants, + // Leave this commented out until we drop support for PHP 8.1. + // + // const VALID_FILE_EXT = ['md', 'bb', 'html']; + + private string $file_name = ''; + 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 + * of the browser. Default to English if both of these are absent. + * + * Updates the `$lang` property of the module. + */ + private function determine_help_language() { + + $language_repository = new LanguageRepository; + $languages = $language_repository->getList(); + + if(array_key_exists(argv(1), $languages)) { + $this->lang['language'] = argv(1); + $this->lang['from_url'] = true; + } else { + if(isset(\App::$language)) { + $this->lang['language'] = \App::$language; + } + + $this->lang['from_url'] = false; + } + } + + /** + * Find the full path name of the file, given it's base path and + * the language of the request. + * + * @param string $base_path The path of the file to find, relative to the + * doc root path, and without the extension. + */ + private function find_help_file(string $base_path, string $lang): void { + + // Use local variable until we can use trait constants. + $valid_file_ext = ['md', 'bb', 'html']; + + $base_path_with_lang = "doc/{$lang}/${base_path}"; + + foreach ($valid_file_ext as $ext) { + $path = "{$base_path_with_lang}.{$ext}"; + if (file_exists($path)) { + $this->file_name = $path; + $this->file_type = $ext; + + 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/developer_guide#Translations for information about how to help.") + ); + } +} diff --git a/Zotlabs/Lib/XConfig.php b/Zotlabs/Lib/XConfig.php index 76ac8dc7a..5eed9224e 100644 --- a/Zotlabs/Lib/XConfig.php +++ b/Zotlabs/Lib/XConfig.php @@ -83,7 +83,7 @@ class XConfig { return $default; if(! array_key_exists($xchan, \App::$config)) - load_xconfig($xchan); + self::Load($xchan); if((! array_key_exists($family, \App::$config[$xchan])) || (! array_key_exists($key, \App::$config[$xchan][$family]))) return $default; diff --git a/Zotlabs/Lib/Zotfinger.php b/Zotlabs/Lib/Zotfinger.php index ccf64d6d1..2a16fc8cf 100644 --- a/Zotlabs/Lib/Zotfinger.php +++ b/Zotlabs/Lib/Zotfinger.php @@ -2,6 +2,7 @@ namespace Zotlabs\Lib; +use Zotlabs\Lib\Config; use Zotlabs\Web\HTTPSig; class Zotfinger { @@ -75,7 +76,7 @@ class Zotfinger { $result['data'] = json_decode($x['body'],true); if($result['data'] && is_array($result['data']) && array_key_exists('encrypted',$result['data']) && $result['data']['encrypted']) { - $result['data'] = json_decode(Crypto::unencapsulate($result['data'],get_config('system','prvkey')),true); + $result['data'] = json_decode(Crypto::unencapsulate($result['data'],Config::Get('system','prvkey')),true); } logger('decrypted: ' . print_r($result,true), LOGGER_DATA); |