Merge branch 'dev' into nomadic

This commit is contained in:
Mike Macgirvin 2024-06-25 08:28:21 +10:00
commit 43fe5d4d15
20 changed files with 1731 additions and 1510 deletions

View file

@ -1,6 +1,8 @@
An open source fediverse server with a long history of innovation. See [FEATURES](https://codeberg.org/streams/streams/src/branch/dev/FEATURES.md).
An open source fediverse server with a long history of innovation. The primary focus is on privacy, consent, resilience, and the corresponding improvements in online safety that this provides.
The software supports a wide range of online behaviour, from personal communications with closed media access - all the way to fully public broadcasting. The default settings tend to favor personal and private use on relatively modest hardware for close friends and family. Adjust as desired.
See [FEATURES](https://codeberg.org/streams/streams/src/branch/dev/FEATURES.md).
The software supports a wide range of online behaviour, from personal communications with restricted media access - all the way to fully public broadcasting. We're big on offering choice; and giving you full control of how you wish to interact and whom you wish to interact with online. The default settings tend to favor personal and private use on relatively modest hardware for close friends and family. Adjust as desired.
This repository uses a community-driven model. This means that there are no dedicated developers working on new features or bug fixes or translations or documentation. Instead, it relies on the contributed efforts of those that choose to use it.

View file

@ -168,7 +168,28 @@ function videowithopts($match)
$poster = 'poster="' . (($zrl) ? zid($matches[1]) : $matches[1]) . '"';
}
return '<video ' . $poster . ' controls="controls" preload="none" src="' . str_replace(' ', '%20', $link) . '" style="width:100%; max-width:100%;"><a href="' . str_replace(' ', '%20', $link) . '">' . $link . '</a></video>';
$x = preg_match("/title='(.*?)'/ism", $attributes, $matches);
if ($x) {
$title = $matches[1];
}
$x = preg_match("/title=\&quot\;(.*?)\&quot\;/ism", $attributes, $matches);
if ($x) {
$title = $matches[1];
}
// This regex permits backslash-quote escapes inside the alt text.
$x = preg_match('/title="([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"/ism', $attributes, $matches);
if ($x) {
$title = $matches[1];
}
$title = (($title)
? 'title="' . htmlspecialchars(str_replace('\\"', '"', $title), ENT_QUOTES, 'UTF-8', false) . '"'
: '');
return '<video ' . $poster . $title . ' controls="controls" preload="none" src="' . str_replace(' ', '%20', $link) . '" style="width:100%; max-width:100%;"><a href="' . str_replace(' ', '%20', $link) . '">' . $link . '</a></video>';
}
// [noparse][i]italic[/i][/noparse] turns into
@ -769,6 +790,18 @@ function bb_map_latlon($match)
$match[0]);
}
function bb_map_link($match)
{
$x = str_replace(['/', ','], [' ', ' '], $match[1]);
$tmp = explode(' ', $x);
if (count($tmp) > 1) {
$lat = $tmp[0];
$lon = $tmp[1];
}
// the extra space in the following line is intentional
return str_replace($match[0], '<a href="' . generate_map_link($lat,$lon) . '">' . t('Show map') . '</a>', $match[0]);
}
function bb_map_coords($match)
{
@ -814,14 +847,9 @@ function bb_spoilertag($match)
function bb_summary($match)
{
$rnd1 = mt_rand();
$rnd2 = mt_rand();
$rnd3 = mt_rand();
$rnd4 = mt_rand();
// return $match[1] . $match[2] . EOL . EOL . $match[3];
return $match[1] . $match[2] . EOL . EOL . $match[3];
// return $match[1] . '<div style="display: block;" id="opendiv-' . $rnd2 . '">' . $match[2] . '</div><div style="display: block;" id="opendiv-' . $rnd3 . '" onclick="openClose(\'opendiv-' . $rnd1 . '\'); openClose(\'opendiv-' . $rnd2 . '\'); openClose(\'opendiv-' . $rnd3 . '\'); openClose(\'opendiv-' . $rnd4 . '\'); return false;" class="fakelink view-article">' . t('View article') . '</div><div style="display: none;" id="opendiv-' . $rnd4 . '" onclick="openClose(\'opendiv-' . $rnd1 . '\'); openClose(\'opendiv-' . $rnd2 . '\'); openClose(\'opendiv-' . $rnd3 . '\'); openClose(\'opendiv-' . $rnd4 . '\'); return false;" class="fakelink view-summary">' . t('View summary') . '</div><div id="opendiv-' . $rnd1 . '" style="display: none;">' . $match[3] . '</div>';
return $match[1] . '<details><summary>' .$match[2] . '</summary>' . '<div>' . $match[3] . '</div></details>';
}
@ -1818,7 +1846,7 @@ function bbcode($Text, $options = [])
if ($export) {
$Text = str_replace([ '[map]','[/map]' ], [ '','' ], $Text);
$Text = preg_replace("/\[map=(.*?)[, ](.*?)\]/ism", 'geo:$1,$2', $Text);
$Text = preg_replace_callback("/\[map=(.*?)\]/ism", 'bb_map_link', $Text);
} else {
if (str_contains($Text, '[/map]')) {
$Text = preg_replace_callback("/\[map\](.*?)\[\/map\]/ism", 'bb_map_location', $Text);

View file

@ -447,7 +447,7 @@ function contact_remove($channel_id, $abook_id, $atoken_sync = false)
Hook::call('connection_remove', $x);
$archive = get_pconfig($channel_id, 'system', 'archive_removed_contacts');
$archive = get_pconfig($channel_id, 'system', 'archive_removed_contacts', true);
if ($archive) {
q(
"update abook set abook_archived = 1 where abook_id = %d and abook_channel = %d",

View file

@ -145,6 +145,10 @@ define ( 'CLIENT_MODE_UPDATE', 0x0002);
define ('AUDIENCE_SENDER', 0x0001);
define ('AUDIENCE_CONVERSATION', 0x0002);
define ('AUDIENCE_FOLLOWERS', 0x0004);
define ('AUDIENCE_PUBLIC', 0x0008);
define ('AUDIENCE_UNLISTED', 0x0010);
define ('AUDIENCE_MENTIONS', 0x0020);
define ('AUDIENCE_SERVER', 0x0040);
/**
*

View file

@ -176,6 +176,20 @@ function purify_html($s, $opts = [])
// disable Unicode version of RTL over-ride
$s = str_replace([ '&#x202e;', '&#x202E;', html_entity_decode('&#x202e;', ENT_QUOTES, 'UTF-8') ], [ '','','' ], $s);
$config->set('URI.AllowedSchemes', [
'data' => true, // This likely won't federate very well, but it has some interesting use cases.
'http' => true,
'https' => true,
'mailto' => true,
'ftp' => true, // This is kind of dubious in 2024, but we'll let it go
'nntp' => false, // he's dead jim
'news' => false, // he's dead jim
'tel' => true,
// @todo create scheme definitions for geo: and ap: and did:
]);
$s = str_replace(['geo:', 'ap:', 'did:'],
['https://example.com/escape/geo', 'https://example.com/escape/ap', 'https://example.com/escape/did' ], $s);
// This will escape invalid tags in the output instead of removing.
// This is necessary for mixed format (text+bbcode+html+markdown) messages or
@ -341,11 +355,17 @@ function purify_html($s, $opts = [])
$purifier = new HTMLPurifier($config);
// HTMLPurifier mangles intentionally escaped alt-text by unescaping it.
// There is an option to encode all non-ascii characters, but we really don't want that either.
// What we really want is for it to just not un-escape escaped double quotes.
$s = str_replace('&quot;','__!ESCAPED_DOUBLEQUOTE!__', $s);
$s = str_replace('__!ESCAPED_DOUBLEQUOTE!__', '&quot;', $purifier->purify($s));
$s = str_replace(['https://example.com/escape/geo', 'https://example.com/escape/ap', 'https://example.com/escape/did' ],
['geo:', 'ap:', 'did:'], $s);
return $s;
}
@ -1741,6 +1761,26 @@ function format_filer(&$item)
return $s;
}
function generate_map_link($lat, $lon, $zoom = 16)
{
$arr = [
'lat' => $lat,
'lon' => $lon,
'zoom' => $zoom,
'link' => ''
];
/**
* @hooks generate_map
* * \e string \b lat
* * \e string \b lon
* * \e string \b html the parsed HTML to return
*/
Hook::call('generate_map_link', $arr);
return (strlen($arr['link'])) ? $arr['link'] : 'geo:' . $lat . ',' . $lon . '&z=' . $zoom;
}
function generate_map($lat, $lon, $zoom = 16)
{
@ -2596,6 +2636,11 @@ function ids_to_querystr($arr, $idx = 'id', $quote = false)
return(implode(',', $t));
}
function array_merge_clean($array1, $array2)
{
return array_values(array_unique(array_merge($array1, $array2)));
}
/**
* @brief array_elm_to_str($arr,$elm,$delim = ',') extract unique individual elements from an array of arrays and return them as a string separated by a delimiter
* similar to ids_to_querystr, but allows a different delimiter instead of a db-quote option

View file

@ -429,7 +429,7 @@ class Notifier implements DaemonInterface
self::$packet_type = 'response';
}
$audience = self::getAudienceFromItem($target_item);
$audience = Activity::getAudienceFromItem($target_item);
$isContained = ($audience & AUDIENCE_CONVERSATION)
|| ($parent_item['item_restrict'] & 4)
@ -894,20 +894,4 @@ class Notifier implements DaemonInterface
}
return $xchans;
}
public static function getAudienceFromItem($item)
{
$audience = 0;
$postopts = explode(',', $item['postopts']);
if (in_array('sender', $postopts)) {
$audience += AUDIENCE_SENDER;
}
if (in_array('conversation', $postopts)) {
$audience += AUDIENCE_CONVERSATION;
}
if (in_array('followers', $postopts)) {
$audience += AUDIENCE_FOLLOWERS;
}
return $audience;
}
}

View file

@ -380,6 +380,7 @@ class Account {
'$sitename' => get_config('system', 'sitename'),
'$siteurl' => z_root(),
'$email' => $arr['email'],
'$reason' => $arr['reason'],
'$uid' => $arr['account']['account_id'],
'$hash' => $hash,
'$details' => $details

View file

@ -978,12 +978,6 @@ class Activity
$activity['inReplyTo'] = set_activity_mid($item['thr_parent']);
}
// @FIXME FEP-5624 set for comment approvals but not event approvals
// For comment approvals and rejections
// if (in_array($activity['type'], ['Accept','Reject']) && is_string($item['obj']) && strlen($item['obj'])) {
// $activity['inReplyTo'] = $item['thr_parent'];
// }
$cnv = get_iconfig($item['parent'], 'activitypub', 'context');
if (!$cnv) {
$cnv = $item['parent_mid'];
@ -1114,82 +1108,8 @@ class Activity
}
}
// addressing madness
if ($activitypub) {
$parent_i = [];
$public = !$item['item_private'];
$top_level = ($item['mid'] === $item['parent_mid']);
$activity['to'] = [];
$activity['cc'] = [];
$recips = get_iconfig($item['parent'], 'activitypub', 'recips');
if ($recips) {
$parent_i['to'] = $recips['to'];
$parent_i['cc'] = $recips['cc'];
}
if ($public) {
$activity['to'] = [ACTIVITY_PUBLIC_INBOX];
if (isset($parent_i['to']) && is_array($parent_i['to'])) {
$activity['to'] = array_values(array_unique(array_merge($activity['to'], $parent_i['to'])));
}
if ($item['item_origin']) {
$activity['cc'] = [Channel::getDidResolver($item['author']) . '/followers'];
}
if (isset($parent_i['cc']) && is_array($parent_i['cc'])) {
$activity['cc'] = array_values(array_unique(array_merge($activity['cc'], $parent_i['cc'])));
}
} else {
// private activity
if ($top_level) {
$activity['to'] = self::map_acl($item);
if (isset($parent_i['to']) && is_array($parent_i['to'])) {
$activity['to'] = array_values(array_unique(array_merge($activity['to'], $parent_i['to'])));
}
} elseif ((int)$item['item_private'] === 1) {
$activity['cc'] = self::map_acl($item);
if (isset($parent_i['cc']) && is_array($parent_i['cc'])) {
$activity['cc'] = array_values(array_unique(array_merge($activity['cc'], $parent_i['cc'])));
}
$d = q(
"select hubloc.* from hubloc left join item on hubloc_hash = owner_xchan where item.parent_mid = '%s' and item.uid = %d and hubloc_deleted = 0 order by hubloc_id desc limit 1",
dbesc($item['parent_mid']),
intval($item['uid'])
);
if ($d) {
if ($d[0]['hubloc_network'] === 'activitypub') {
$addr = $d[0]['hubloc_hash'];
} else {
$addr = $d[0]['hubloc_id_url'];
}
$activity['cc'][] = $addr;
}
}
}
$mentions = self::map_mentions($item);
if (count($mentions) > 0) {
if (!$activity['to']) {
$activity['to'] = $mentions;
} else {
$activity['to'] = array_values(array_unique(array_merge($activity['to'], $mentions)));
}
}
}
$cc = [];
if ($activity['cc'] && is_array($activity['cc'])) {
foreach ($activity['cc'] as $e) {
if (!is_array($activity['to'])) {
$cc[] = $e;
} elseif (!in_array($e, $activity['to'])) {
$cc[] = $e;
}
}
}
$activity['cc'] = $cc;
$activity = self::encodeAddressing($activity, $item);
return $activity;
}
@ -1218,6 +1138,7 @@ class Activity
$activity['to'] = [ACTIVITY_PUBLIC_INBOX];
return $activity;
}
$activity['replies'] = z_root() . '/replies/' . basename($item['mid']);
if (isset($item['obj']) && $item['obj']) {
if (is_string($item['obj'])) {
@ -1480,9 +1401,32 @@ class Activity
}
}
// addressing madness
$activity = self::encodeAddressing($activity, $item);
return $activity;
}
public static function encodeAddressing($activity,$item)
{
/*
* addressing madness
*
* For what it's worth, Mastodons visibility scopes are a custom thing and this is generally how they map:
* public = to: [public], cc: [followers, mentions]
* unlisted = cc: [public, followers, mentions]
* followers-only = to: [followers], cc: [mentions]
* direct = to: [mentions]
*
* This software does not normally use the same mappings - and doesn't even require mappings;
* but the Mastodon mappings may need to be considered so that private audiences are not mis-identified
* when communicating with people using that software.
*/
$channel = Channel::from_id($item['uid']); // needed in future for nomadic follower addressing
$audience = self::getAudienceFromItem($item);
$mentions = self::map_mentions($item);
$followers = [z_root() . '/followers/' . substr($item['author']['xchan_addr'], 0, strpos($item['author']['xchan_addr'], '@'))];
if ($activitypub) {
$parent_i = [];
$activity['to'] = [];
$activity['cc'] = [];
@ -1510,30 +1454,32 @@ class Activity
}
}
if ($public) {
$activity['to'] = [ACTIVITY_PUBLIC_INBOX];
if (isset($parent_i['to']) && is_array($parent_i['to'])) {
$activity['to'] = array_values(array_unique(array_merge($activity['to'], $parent_i['to'])));
$activity['to'] = array_merge_clean($activity['to'], $parent_i['to']);
}
if ($item['item_origin']) {
$activity['cc'] = [Channel::getDidResolver($item['author']) . '/followers'];
$activity['cc'] = $followers;
}
if (isset($parent_i['cc']) && is_array($parent_i['cc'])) {
$activity['cc'] = array_values(array_unique(array_merge($activity['cc'], $parent_i['cc'])));
$activity['cc'] = array_merge_clean($activity['cc'], $parent_i['cc']);
}
} else {
// private activity
if ($top_level) {
$activity['to'] = self::map_acl($item);
if (isset($parent_i['to']) && is_array($parent_i['to'])) {
$activity['to'] = array_values(array_unique(array_merge($activity['to'], $parent_i['to'])));
$activity['to'] = array_merge_clean($activity['to'], $parent_i['to']);
}
} elseif ((int)$item['item_private'] === 1) {
if (($audience & AUDIENCE_FOLLOWERS) && $item['item_origin']) {
$activity['to'] = $followers;
} else {
$activity['cc'] = self::map_acl($item);
if (isset($parent_i['cc']) && is_array($parent_i['cc'])) {
$activity['cc'] = array_values(array_unique(array_merge($activity['cc'], $parent_i['cc'])));
$activity['cc'] = array_merge_clean($activity['cc'], $parent_i['cc']);
}
$d = q(
@ -1543,7 +1489,7 @@ class Activity
);
if ($d) {
if ($d[0]['hubloc_network'] === 'activitypub') {
if (in_array($d[0]['hubloc_network'], ['activitypub', 'apnomadic'])) {
$addr = $d[0]['hubloc_hash'];
} else {
$addr = $d[0]['hubloc_id_url'];
@ -1552,13 +1498,17 @@ class Activity
}
}
}
}
$mentions = self::map_mentions($item);
if (count($mentions) > 0) {
// Delivery is controlled by parent ACL when AUDIENCE_CONVERSATION is in effect -
// and mentions are ignored.
if ((!$audience) || !($audience & AUDIENCE_CONVERSATION)) {
if (count($mentions)) {
if (!$activity['to']) {
$activity['to'] = $mentions;
} else {
$activity['to'] = array_values(array_unique(array_merge($activity['to'], $mentions)));
} elseif ((int)$item['item_private'] !== 2) {
$activity['cc'] = array_merge_clean($activity['cc'], $mentions);
}
}
}
@ -1582,6 +1532,8 @@ class Activity
}
// Returns an array of URLS for any mention tags found in the item array $i.
public static function map_mentions($i)
@ -4815,9 +4767,15 @@ class Activity
}
if (array_key_exists('type', $a) && stripos($a['type'], 'video') !== false) {
if (self::media_not_in_body($a['href'], $item['body'])) {
if (isset($a['name']) && $a['name']) {
$alt = htmlspecialchars($a['name'], ENT_QUOTES, 'UTF-8', false);
$alt = str_replace(['[', ']'], ['&#xFF3B;', '&#xFF3D;'], $alt);
$item['body'] .= "\n\n" . '[video title="' . $alt . '"]' . $a['href'] . '[/img]';
} else {
$item['body'] .= "\n\n" . '[video]' . $a['href'] . '[/video]';
}
}
}
if (array_key_exists('type', $a) && stripos($a['type'], 'audio') !== false) {
if (self::media_not_in_body($a['href'], $item['body'])) {
$item['body'] .= "\n\n" . '[audio]' . $a['href'] . '[/audio]';
@ -5310,4 +5268,34 @@ class Activity
}
return $item;
}
public static function getPostOpts($item)
{
return explode(',', $item['postopts']);
}
public static function getAudienceFromItem($item)
{
$audience = 0;
$postopts = self::getPostOpts($item);
if (in_array('sender', $postopts)) {
$audience += AUDIENCE_SENDER;
}
if (in_array('conversation', $postopts)) {
$audience += AUDIENCE_CONVERSATION;
}
if (in_array('followers', $postopts)) {
$audience += AUDIENCE_FOLLOWERS;
}
if (in_array('public', $postopts)) {
$audience += AUDIENCE_PUBLIC;
}
if (in_array('unlisted', $postopts)) {
$audience += AUDIENCE_UNLISTED;
}
if (in_array('mentions', $postopts)) {
$audience += AUDIENCE_MENTIONS;
}
return $audience;
}
}

View file

@ -144,8 +144,17 @@ class Enotify
$preamble = sprintf(t('%1$s sent you a new private message at %2$s.'), $sender['xchan_name'], $sitename);
$epreamble = sprintf(t('%1$s sent you %2$s.'), '[zrl=' . $sender['xchan_url'] . ']' . html_entity_decode('&#8203;') . $sender['xchan_name'] . '[/zrl]', '[zrl=$itemlink]' . t('a private message') . '[/zrl]');
} else {
$preamble = sprintf(t('%1$s replied to a private message at %2$s.'), $sender['xchan_name'], $sitename);
$epreamble = sprintf(t('%1$s replied to %2$s.'), '[zrl=' . $sender['xchan_url'] . ']' . html_entity_decode('&#8203;') . $sender['xchan_name'] . '[/zrl]', '[zrl=$itemlink]' . t('a private message') . '[/zrl]');
if (activity_match($params['item']['verb'], ACTIVITY_LIKE)) {
$action = t('liked');
}
elseif (activity_match($params['item']['verb'], ACTIVITY_DISLIKE)) {
$action = t('disliked');
}
else {
$action = t('replied to');
}
$preamble = sprintf(t('%1$s %2$s a private message at %3$s.'), $sender['xchan_name'], $action, $sitename);
$epreamble = sprintf(t('%1$s %2$s %3$s.', 'react_private_message'), '[zrl=' . $sender['xchan_url'] . ']' . html_entity_decode('&#8203;') . $sender['xchan_name'] . '[/zrl]', $action, '[zrl=$itemlink]' . t('a private message') . '[/zrl]');
}
$sitelink = t('Please visit %s to view and/or reply to your private messages.');

View file

@ -23,15 +23,6 @@ class Dirsearch extends Controller
// logger('request: ' . print_r($_REQUEST,true));
if (argc() > 1 && argv(1) === 'sites') {
$ret = $this->list_public_sites();
json_return_and_die($ret);
}
$dirmode = intval(get_config('system', 'directory_mode'));
$network = EMPTY_STR;
$sql_extra = '';
@ -90,19 +81,6 @@ class Dirsearch extends Controller
$safe = 1;
}
// Directory mirrors will request sync packets, which are lists
// of records that have changed since the sync datetime.
if (array_key_exists('sync', $_REQUEST)) {
if ($_REQUEST['sync']) {
$sync = Time::convert('UTC', 'UTC', $_REQUEST['sync']);
} else {
$sync = Time::convert('UTC', 'UTC', '2010-01-01 01:01:00');
}
} else {
$sync = false;
}
if ($hub) {
$hub_query = " and xchan_hash in (select hubloc_hash from hubloc where hubloc_deleted = 0 and hubloc_host = '" . protect_sprintf(dbesc($hub)) . "') ";
} else {
@ -410,45 +388,4 @@ class Dirsearch extends Controller
return $ret;
}
public function list_public_sites()
{
$rand = db_getfunc('rand');
$r = q(
"select * from site where site_type = %d and site_dead = 0",
intval(SITE_TYPE_ZOT)
);
$ret = ['success' => false];
if ($r) {
$ret['success'] = true;
$ret['sites'] = [];
foreach ($r as $rr) {
if ($rr['site_access'] == ACCESS_FREE) {
$access = 'free';
} elseif ($rr['site_access'] == ACCESS_PAID) {
$access = 'paid';
} elseif ($rr['site_access'] == ACCESS_TIERED) {
$access = 'tiered';
} else {
$access = 'private';
}
if ($rr['site_register'] == REGISTER_OPEN) {
$register = 'open';
} elseif ($rr['site_register'] == REGISTER_APPROVE) {
$register = 'approve';
} else {
$register = 'closed';
}
$ret['sites'][] = ['url' => $rr['site_url'], 'access' => $access, 'register' => $register, 'sellpage' => $rr['site_sellpage'], 'location' => $rr['site_location'], 'project' => $rr['site_project'], 'version' => $rr['site_version']];
}
}
return $ret;
}
}

View file

@ -63,13 +63,13 @@ class Inspect extends Controller
$output .= '(encode_activity)' . EOL . '<pre>' . escape_tags(json_encode(Activity::encode_activity($item, true),
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) . '</pre>' . EOL . EOL;
$output .= '(cached activity)' . EOL . '<pre>' . jindent(escape_tags(ObjCache::Get($item['mid']) ?? '')) . '</pre>' . EOL . EOL;
$output .= '(cached activity)' . EOL . '<pre>' . escape_tags(json_encode(json_decode(ObjCache::Get($item['mid']) ?? '', true),JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) . '</pre>' . EOL . EOL;
if (str_contains($item['tgt_type'], 'Collection') && !empty($item['target']['id'])) {
$str = Activity::fetch($item['target']['id'], App::$channel);
if ($str) {
$output .= '(conversation)' . EOL . '<pre>' .
json_encode($str, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT) .
escape_tags(json_encode($str, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)) .
'</pre>' . EOL . EOL;
}
}

View file

@ -181,6 +181,7 @@ class Register extends Controller
}
}
} elseif ($policy == REGISTER_APPROVE) {
$result['reason'] = substr(trim(escape_tags($_REQUEST['reason'])),0, 500);
$res = Account::send_reg_approval_email($result);
if ($res) {
info(t('Your registration is pending approval by the site owner.') . EOL);

181
src/Module/Replies.php Normal file
View file

@ -0,0 +1,181 @@
<?php
namespace Code\Module;
use Code\Web\Controller;
use Code\Lib\ActivityStreams;
use Code\Lib\Activity as ZlibActivity;
use Code\Lib\Libzot;
use Code\Web\HTTPSig;
use Code\Lib\ThreadListener;
use Code\Lib\Channel;
use App;
class Replies extends Controller
{
public function init()
{
if (ActivityStreams::is_as_request() || Libzot::is_nomad_request()) {
$item_mid = argv(1);
if (!$item_mid) {
http_status_exit(404, 'Not found');
}
$portable_id = EMPTY_STR;
$item_normal = " and item.item_hidden = 0 and item.item_type = 0 and item.item_unpublished = 0 and item.item_delayed = 0 and item.item_blocked = 0 and not verb in ( 'Follow', 'Ignore' ) ";
$i = null;
// do we have the item (at all)?
$test = q(
"select * from item where mid = '%s' $item_normal limit 1",
dbesc(z_root() . '/activity/' . $item_mid)
);
if (!$test) {
$test = q(
"select * from item where mid = '%s' $item_normal limit 1",
dbesc(z_root() . '/item/' . $item_mid)
);
if (!$test) {
http_status_exit(404, 'Not found');
}
}
// process an authenticated fetch
$zot6Hubloc = false;
$sigdata = HTTPSig::verify(EMPTY_STR);
if ($sigdata['portable_id'] && $sigdata['header_valid']) {
$portable_id = $sigdata['portable_id'];
observer_auth($portable_id);
// first see if we have a copy of this item owned by the current signer
// include xchans for all zot-like networks - these will have the same guid and public key
$x = q(
"select * from xchan where xchan_hash = '%s'",
dbesc($sigdata['portable_id'])
);
if ($x) {
$xchans = q(
"select xchan_hash from xchan where xchan_hash = '%s' OR ( xchan_guid = '%s' AND xchan_pubkey = '%s' ) ",
dbesc($sigdata['portable_id']),
dbesc($x[0]['xchan_guid']),
dbesc($x[0]['xchan_pubkey'])
);
if ($xchans) {
$hashes = ids_to_querystr($xchans, 'xchan_hash', true);
$i = q(
"select item.* id as item_id from item where mid = '%s' $item_normal and owner_xchan in ( " . protect_sprintf($hashes) . " ) limit 1",
dbesc($test[0]['mid'])
);
}
}
}
// if we don't have a parent id belonging to the signer see if we can obtain one as a visitor that we have permission to access
// with a bias towards those items owned by channels on this site (item_wall = 1)
$sql_extra = item_permissions_sql(0);
if (!$i) {
$i = q(
"select item.*, id as item_id from item where mid = '%s' $item_normal $sql_extra order by item_wall desc limit 1",
dbesc($test[0]['mid'])
);
}
if (!$i) {
http_status_exit(403, 'Forbidden');
}
xchan_query($i);
$sql_noAdd = " and verb not in ('Add', 'Remove') ";
$items = q(
"SELECT item.*, item.id AS item_id FROM item WHERE item.thr_parent = '%s' and item.mid != item.parent_mid $item_normal $sql_noAdd and item_private = %d and item_deleted = 0 and uid = %d",
dbesc($i[0]['mid']),
intval($i[0]['item_private']),
intval($i[0]['uid'])
);
if (!$items) {
http_status_exit(404, 'Not found');
}
xchan_query($items, true);
$items = fetch_post_tags($items);
$observer = App::get_observer();
$parent = $i[0];
$recips = (($parent['owner']['xchan_network'] === 'activitypub') ? get_iconfig($parent['id'], 'activitypub', 'recips', []) : []);
$to = (($recips && array_key_exists('to', $recips) && is_array($recips['to'])) ? $recips['to'] : null);
$nitems = [];
foreach ($items as $item) {
$mids = [];
if (intval($item['item_private'])) {
if (!$observer) {
continue;
}
// ignore private reshare, possibly from hubzilla
if ($i['verb'] === 'Announce') {
if (!in_array($item['thr_parent'], $mids)) {
$mids[] = $item['thr_parent'];
}
continue;
}
// also ignore any children of the private reshares
if (in_array($item['thr_parent'], $mids)) {
continue;
}
if ($observer['xchan_hash'] !== $item['owner_xchan']) {
if (empty($to)) {
continue;
}
if (!in_array($observer['xchan_url'], $to)) {
continue;
}
}
}
$nitems[] = $item;
}
if (!$nitems) {
http_status_exit(404, 'Not found');
}
$chan = Channel::from_id($i[0]['uid']);
if (!$chan) {
http_status_exit(404, 'Not found');
}
if (!perm_is_allowed($chan['channel_id'], get_observer_hash(), 'view_stream')) {
http_status_exit(403, 'Forbidden');
}
$collection = ZlibActivity::encode_item_collection($nitems, 'replies/' . $item_mid, 'OrderedCollection', true, z_root() . '/channel/' . $chan['channel_address'], count($nitems));
if ($portable_id && (!intval($items[0]['item_private']))) {
ThreadListener::store(z_root() . '/activity/' . $item_mid, $portable_id);
}
if (!$collection) {
http_status_exit(404, 'Not found');
}
$channel = Channel::from_id($i[0]['uid']);
as_return_and_die($collection, $channel);
}
goaway(z_root() . '/item/' . argv(1));
}
}

View file

@ -2,6 +2,7 @@
namespace Code\Module;
use Code\Lib\ActivityStreams;
use Code\Web\Controller;
use Code\Lib\System;
use Code\Lib\Channel;
@ -38,7 +39,7 @@ class Webfinger extends Controller
$resource = $_REQUEST['resource'];
if (!$resource) {
if (!$resource || !ActivityStreams::is_url($resource)) {
http_status_exit(400, 'Bad request');
}

File diff suppressed because it is too large Load diff

View file

@ -1,2 +1,2 @@
<?php
define ('STD_VERSION', '24.06.14');
define ('STD_VERSION', '24.06.22');

View file

@ -2,13 +2,15 @@
A new user registration request was received at {{$sitename}} which requires
your approval.
The login details are as follows:
Site Location: {{$siteurl}}
Login Name: {{$email}}
IP Address: {{$details}}
Reason:
{{$reason}}
To approve this request please visit the following link:

View file

@ -9,6 +9,10 @@ Site Location: {{$siteurl}}
Login Name: {{$email}}
IP Address: {{$details}}
Reason:
{{$reason}}
To approve this request please visit the following link:

View file

@ -2,13 +2,16 @@
A new user registration request was received at {{$sitename}} which requires
your approval.
The login details are as follows:
Site Location: {{$siteurl}}
Login Name: {{$email}}
IP Address: {{$details}}
Reason:
{{$reason}}
To approve this request please visit the following link:

View file

@ -1,5 +1,6 @@
[region=aside]
[widget=vcard][/widget]
[widget=tasklist][/widget]
[/region]
[region=right_aside]
[widget=notifications][/widget]