Merge branch 'dev' into release

This commit is contained in:
nobody 2022-03-16 21:36:59 -07:00
commit 7cac6004cf
17 changed files with 255 additions and 117 deletions

View file

@ -3,6 +3,7 @@
namespace Code\Extend;
use App;
use DBA;
/**
* @brief Hook class.

View file

@ -625,7 +625,7 @@ class Account {
Channel::auto_create($register[0]['uid']);
} else {
$_SESSION['login_return_url'] = 'new_channel';
authenticate_success($account[0], null, true, true, false, true);
authenticate_success($account[0], false, true, true, false, true);
}
return true;

View file

@ -778,13 +778,14 @@ class Activity
if (!in_array($ret['type'], ['Create', 'Update', 'Accept', 'Reject', 'TentativeAccept', 'TentativeReject'])) {
$ret['inReplyTo'] = $i['thr_parent'];
$cnv = get_iconfig($i['parent'], 'activitypub', 'context');
if (!$cnv) {
$cnv = get_iconfig($i['parent'], 'ostatus', 'conversation');
}
if (!$cnv) {
$cnv = $ret['parent_mid'];
}
}
$cnv = get_iconfig($i['parent'], 'activitypub', 'context');
if (!$cnv) {
$cnv = get_iconfig($i['parent'], 'ostatus', 'conversation');
}
if (!$cnv) {
$cnv = $ret['parent_mid'];
}
}
@ -3822,17 +3823,24 @@ class Activity
$item['allow_gid'] = $item['deny_cid'] = $item['deny_gid'] = '';
}
}
// Private conversation, but this comment went rogue and was published publicly
// Set item_restrict to indicate this condition so we can flag it in the UI
if (intval($parent[0]['item_private']) !== 0 && $act->recips && (in_array(ACTIVITY_PUBLIC_INBOX, $act->recips) || in_array('Public', $act->recips) || in_array('as:Public', $act->recips))) {
$item['item_restrict'] = $item['item_restrict'] | 2;
}
}
self::rewrite_mentions($item);
if (! isset($item['replyto'])) {
if (strpos($item['owner_xchan'],'http') === 0) {
$item['replyto'] = $item['owner_xchan'];
}
else {
$r = q("select hubloc_id_url from hubloc where hubloc_hash = '%s' and hubloc_primary = 1",
dbesc($item['owner_xchan'])
);
if ($r) {
$item['replyto'] = $r[0]['hubloc_id_url'];
}
}
}
$r = q(
"select id, created, edited from item where mid = '%s' and uid = %d limit 1",
dbesc($item['mid']),
@ -3887,13 +3895,16 @@ class Activity
// We are the owner of this conversation, so send all received comments back downstream
Run::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]);
}
}
elseif ($act->client && $channel['channel_hash'] === $observer_hash) {
Run::Summon(['Notifier', 'wall-new', $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']);
}

View file

@ -16,6 +16,7 @@ class ActivityStreams
public $data = null;
public $meta = null;
public $hub = null;
public $client = false;
public $valid = false;
public $deleted = false;
public $id = '';
@ -48,7 +49,8 @@ class ActivityStreams
$this->raw = $string;
$this->hub = $hub;
$this->client = $client;
if (is_array($string)) {
$this->data = $string;
$this->raw = json_encode($string, JSON_UNESCAPED_SLASHES);

View file

@ -219,7 +219,8 @@ class Item extends Controller
dbesc($r[0]['parent_mid']),
dbesc($portable_id)
);
} elseif (Config::get('system', 'require_authenticated_fetch', false)) {
}
elseif (Config::get('system', 'require_authenticated_fetch', false)) {
http_status_exit(403, 'Permission denied');
}
@ -738,7 +739,7 @@ class Item extends Controller
dbesc($channel['channel_hash'])
);
if ($r && count($r)) {
$owner_xchan = $r[0];
$owner_xchan = array_shift($r);
} else {
logger("mod_item: no owner.");
if ($api_source) {
@ -775,15 +776,6 @@ class Item extends Controller
}
}
if (!isset($replyto)) {
if (strpos($owner_xchan['xchan_hash'], 'http') === 0) {
$replyto = $owner_xchan['xchan_hash'];
} else {
$replyto = $owner_xchan['xchan_url'];
}
}
$acl = new AccessControl($channel);
$view_policy = PermissionLimits::Get($channel['channel_id'], 'view_stream');
@ -1396,6 +1388,24 @@ class Item extends Controller
$datarray['obj']['id'] = $mid;
}
if (! (isset($replyto) && $replyto)) {
if ($owner_hash && strpos($owner_hash,'http') === 0) {
$replyto = $owner_hash;
}
else {
$tmp = $owner_hash ? $owner_hash : $owner_xchan['xchan_hash'];
if ($tmp) {
$r = q("select hubloc_id_url from hubloc where hubloc_hash = '%s' and hubloc_primary = 1",
dbesc($tmp)
);
if ($r) {
$replyto = $r[0]['hubloc_id_url'];
}
}
}
}
if ($private && !$parent) {
if ( intval($private) === 1 && (!$str_group_allow) && in_array(substr_count($str_contact_allow,'<'), [ 1, 2 ])) {
$private = 2;

View file

@ -98,7 +98,7 @@ class Lockview extends Controller
$l = array_merge($l, $recips['cc']);
}
for ($x = 0; $x < count($l); $x++) {
if ($l[$x] === ACTIVITY_PUBLIC_INBOX) {
if ($l[$x] === ACTIVITY_PUBLIC_INBOX || $l[$x] === 'Public' || $l[$x] === 'as:Public') {
$l[$x] = '<strong><em>' . t('Everybody') . '</em></strong>';
} else {
$l[$x] = '<a href="' . $l[$x] . '">' . $l[$x] . '</a>';

View file

@ -11,20 +11,34 @@ use Code\Web\HTTPSig;
use Code\Lib\Activity;
use Code\Lib\ActivityPub;
use Code\Lib\Config;
use Code\Lib\PConfig;
use Code\Lib\Channel;
require_once('include/api_auth.php');
require_once('include/api.php');
/**
* Implements an ActivityPub outbox.
*/
class Outbox extends Controller
{
public function init() {
if (! api_user()) {
api_login();
}
}
public function post()
{
if (argc() < 2) {
killme();
}
if (! api_user()) {
killme();
}
$channel = Channel::from_username(argv(1));
if (!$channel) {
killme();
@ -34,25 +48,20 @@ class Outbox extends Controller
killme();
}
// At this point there is unlikely to be an authenticated observer using the C2S ActivityPub API.
// Mostly we're protecting the page from malicious mischief until the project's OAuth2 interface
// is linked to this page.
$observer = App::get_observer();
if (!$observer) {
killme();
}
if ($observer['xchan_hash'] !== $channel['channel_hash']) {
if (!perm_is_allowed($channel['channel_id'], $observer['xchan_hash'], 'post_wall')) {
logger('outbox post permission denied to ' . $observer['xchan_name']);
killme();
}
}
// disable C2S until we've fully tested it.
return;
$observer_hash = get_observer_hash();
$data = file_get_contents('php://input');
if (!$data) {
return;
@ -60,7 +69,8 @@ class Outbox extends Controller
logger('outbox_activity: ' . jindent($data), LOGGER_DATA);
$AS = new ActivityStreams($data);
// the third parameter signals to the parser that we are using C2S and that implied Create activities are supported
$AS = new ActivityStreams($data, null, true);
if (!$AS->is_valid()) {
return;
@ -70,41 +80,57 @@ class Outbox extends Controller
return;
}
// ensure the posted activity has required attributes
$uuid = new_uuid();
if (! $AS->id) {
$AS->id = z_root() . '/activity/' . $uuid;
}
if (isset($AS->obj) && (! isset($AS->obj['id']))) {
$AS->obj['id'] = z_root() . '/item/' . $uuid;
}
if (! isset($AS->actor)) {
$AS->actor = Channel::url($channel);
}
logger('outbox_channel: ' . $channel['channel_address'], LOGGER_DEBUG);
// switch ($AS->type) {
// case 'Follow':
// if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && ActivityStreams::is_an_actor($AS->obj['type']) && isset($AS->obj['id'])) {
// // do follow activity
// Activity::follow($channel,$AS);
// }
// break;
// case 'Invite':
// if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && $AS->obj['type'] === 'Group') {
// // do follow activity
// Activity::follow($channel,$AS);
// }
// break;
// case 'Join':
// if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && $AS->obj['type'] === 'Group') {
// // do follow activity
// Activity::follow($channel,$AS);
// }
// break;
// case 'Accept':
// // Activitypub for wordpress sends lowercase 'follow' on accept.
// // https://github.com/pfefferle/wordpress-activitypub/issues/97
// // Mobilizon sends Accept/"Member" (not in vocabulary) in response to Join/Group
// if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && in_array($AS->obj['type'], ['Follow','follow', 'Member'])) {
// // do follow activity
// Activity::follow($channel,$AS);
// }
// break;
// case 'Reject':
// default:
// break;
//
// }
switch ($AS->type) {
case 'Follow':
if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && ActivityStreams::is_an_actor($AS->obj['type']) && isset($AS->obj['id'])) {
// do follow activity
Activity::follow($channel,$AS);
}
break;
case 'Invite':
if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && $AS->obj['type'] === 'Group') {
// do follow activity
Activity::follow($channel,$AS);
}
break;
case 'Join':
if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && $AS->obj['type'] === 'Group') {
// do follow activity
Activity::follow($channel,$AS);
}
break;
case 'Accept':
// Activitypub for wordpress sends lowercase 'follow' on accept.
// https://github.com/pfefferle/wordpress-activitypub/issues/97
// Mobilizon sends Accept/"Member" (not in vocabulary) in response to Join/Group
if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && in_array($AS->obj['type'], ['Follow','follow', 'Member'])) {
// do follow activity
Activity::follow($channel,$AS);
}
break;
case 'Reject':
default:
break;
}
// These activities require permissions
@ -113,10 +139,7 @@ class Outbox extends Controller
switch ($AS->type) {
case 'Update':
if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && ActivityStreams::is_an_actor($AS->obj['type'])) {
// pretend this is an old cache entry to force an update of all the actor details
$AS->obj['cached'] = true;
$AS->obj['updated'] = datetime_convert('UTC', 'UTC', '1980-01-01', ATOM_TIME);
Activity::actor_store($AS->obj['id'], $AS->obj);
Activity::actor_store($AS->obj['id'], $AS->obj, true /* force cache refresh */);
break;
}
case 'Accept':
@ -186,8 +209,69 @@ class Outbox extends Controller
}
if ($item) {
// fixup some of the item fields when using C2S
if (! (isset($item['parent_mid']) && $item['parent_mid'])) {
$item['parent_mid'] = $item['mid'];
}
// map ActivityPub recipients to Nomad ACLs to the extent possible.
if (isset($AS->recips)) {
$item['item_private'] = ((in_array(ACTIVITY_PUBLIC_INBOX, $AS->recips)
|| in_array('Public', $AS->recips)
|| in_array('as:Public', $AS->recips))
? 0
: 1
);
if ($item['item_private']) {
foreach ($AS->recips as $recip) {
if (strpos($recip,'/lists/')) {
$r = q("select * from pgrp where hash = '%s' and uid = %d",
dbesc(basename($recip)),
intval($channel['channel_id'])
);
if ($r) {
if (! isset($item['allow_gid'])) {
$item['allow_gid'] = EMPTY_STR;
}
$item['allow_gid'] .= '<' . $r[0]['hash'] . '>';
}
continue;
}
if ($recip === z_root() . '/followers/' . $channel['channel_address']) {
// map to a virtual list/group even if the app isn't installed. This should do the right
// thing and create a followers-only post with the correct ACL as long as the public stream
// isn't addressed. And if it is, the post will still go to all your connections - so the ACL isn't
// necessary.
if (! isset($item['allow_gid'])) {
$item['allow_gid'] = EMPTY_STR;
}
$item['allow_gid'] .= '<connections:' . $channel['channel_hash'] . '>';
continue;
}
$r = q("select * from hubloc where hubloc_id_url = '%s'",
dbesc($recip)
);
if ($r) {
if (! isset($item['allow_cid'])) {
$item['allow_cid'] = EMPTY_STR;
}
$item['allow_cid'] .= '<' . $r[0]['hubloc_hash'] . '>';
}
}
}
// set the DM flag if needed
if ($item['item_private'] && isset($item['allow_cid']) && ! isset($item['allow_gid'])
&& in_array(substr_count($item['allow_cid'],'<'), [ 1, 2 ])) {
$item['item_private'] = 2;
}
}
$item['item_wall'] = 1;
logger('parsed_item: ' . print_r($item, true), LOGGER_DATA);
Activity::store($channel, $observer_hash, $AS, $item);
}
http_status_exit(200, 'OK');

View file

@ -174,7 +174,7 @@ class Register extends Controller
// fall through and authenticate if no approvals or verifications were required.
authenticate_success($result['account'], null, true, false, true);
authenticate_success($result['account'], false, true, false, true);
$new_channel = false;
$next_page = 'new_channel';

View file

@ -12,6 +12,8 @@ use Code\Daemon\Run;
use Code\Lib\Channel;
use Code\Lib\Navbar;
use Code\Render\Theme;
use Code\Lib\LDSignatures;
use Code\Web\HTTPSig;
require_once("include/bbcode.php");
@ -52,7 +54,7 @@ class Search extends Controller
}
Navbar::set_selected('Search');
$format = (($_REQUEST['format']) ? $_REQUEST['format'] : '');
$format = (($_REQUEST['module_format']) ? $_REQUEST['module_format'] : '');
if ($format !== '') {
$this->updating = $this->loading = 1;
}
@ -66,15 +68,19 @@ class Search extends Controller
if (x(App::$data, 'search')) {
$search = trim(App::$data['search']);
$saved_id = 'search=' . urlencode($_GET['search']);
} else {
$search = ((x($_GET, 'search')) ? trim(escape_tags(rawurldecode($_GET['search']))) : '');
$saved_id = 'search=' . urlencode($_GET['search']);
}
$tag = false;
if (x($_GET, 'tag')) {
$tag = true;
$search = ((x($_GET, 'tag')) ? trim(escape_tags(rawurldecode($_GET['tag']))) : '');
$saved_id = 'tag=' . urlencode($_GET['tag']);
}
$static = ((array_key_exists('static', $_REQUEST)) ? intval($_REQUEST['static']) : 0);
$o .= search($search, 'search-box', '/search', ((local_channel()) ? true : false));
@ -199,21 +205,21 @@ class Search extends Controller
$tag = true;
$search = substr($search, 1);
}
if (strpos($search, '@') === 0) {
if (strpos($search, '@') === 0 && $format === '') {
$search = substr($search, 1);
goaway(z_root() . '/directory' . '?f=1&navsearch=1&search=' . $search);
}
if (strpos($search, '!') === 0) {
if (strpos($search, '!') === 0 && $format === '') {
$search = substr($search, 1);
goaway(z_root() . '/directory' . '?f=1&navsearch=1&search=' . $search);
}
if (strpos($search, '?') === 0) {
if (strpos($search, '?') === 0 && $format === '') {
$search = substr($search, 1);
goaway(z_root() . '/help' . '?f=1&navsearch=1&search=' . $search);
}
// look for a naked webbie
if (strpos($search, '@') !== false && strpos($search, 'http') !== 0) {
if (strpos($search, '@') !== false && strpos($search, 'http') !== 0 && $format === '') {
goaway(z_root() . '/directory' . '?f=1&navsearch=1&search=' . $search);
}
@ -335,16 +341,28 @@ class Search extends Controller
$items = [];
}
if ($format == 'json') {
$result = [];
require_once('include/conversation.php');
foreach ($items as $item) {
$item['html'] = zidify_links(bbcode($item['body']));
$x = encode_item($item);
$x['html'] = prepare_text($item['body'], $item['mimetype']);
$result[] = $x;
}
json_return_and_die(array('success' => true, 'messages' => $result));
if ($format === 'json') {
$chan = Channel::get_system();
$i = Activity::encode_item_collection($items, 'search?' . $saved_id , 'OrderedCollection', true, count($items));
$x = array_merge(['@context' => [
ACTIVITYSTREAMS_JSONLD_REV,
'https://w3id.org/security/v1',
Activity::ap_schema()
]], $i);
$headers = [];
$headers['Content-Type'] = 'application/x-nomad+json';
$x['signature'] = LDSignatures::sign($x, $chan);
$ret = json_encode($x, JSON_UNESCAPED_SLASHES);
$headers['Digest'] = HTTPSig::generate_digest_header($ret);
$headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI'];
$h = HTTPSig::create_sig($headers, $chan['channel_prvkey'], Channel::url($chan));
HTTPSig::set_headers($h);
echo $ret;
killme();
}
if ($tag) {

View file

@ -3,7 +3,19 @@ Federation
The ActivityPub implementation in this project strives to be compliant to the core spec where possible, while offering a range of services and features which normally aren't provided by ActivityPub projects.
C2S
This project supports ActivityPub C2S. You may authenticate with HTTP basic-auth, OAuth2, or OpenWebAuth. There is no media upload endpoint since the (deprecated) specification of that service has no workarounds for working in memory-restricted environments and most mobile phone photos exceed PHP's default upload size limits.
Client search interface
If public access is allowed to the content search interface (a site security setting), clients may search the content of public messages or tags and are returned an ActivityStreams Collection of search results. When authenticated via OpenWebAuth, the search results may contain their own content or private content which they are permitted to access.
The URL endpoints are:
https://example.com/search?search=banana
https://example.com/search?tag=banana
Direct Messages
Direct Messages (DM) are differentiated from other private messaging using the zot:directMessage flag (boolean). This is compatible with the same facility provided by other projects in other namespaces and is not prefixed within activities so that these may potentially be aggregated.

View file

@ -14,7 +14,7 @@ require_once('include/security.php');
/**
* API Login via basic-auth or OAuth
* API Login via basic-auth, OpenWebAuth, or OAuth2
*/
function api_login()
@ -58,8 +58,7 @@ function api_login()
intval($record['channel_account_id'])
);
if ($x) {
require_once('include/security.php');
authenticate_success($x[0], null, true, false, true, true);
authenticate_success($x[0], false, true, false, true, true);
$_SESSION['allow_api'] = true;
Hook::call('logged_in', App::$user);
return;
@ -164,8 +163,8 @@ function api_login()
function retry_basic_auth($method = 'Basic')
{
header('WWW-Authenticate: ' . $method . ' realm="' . System::get_platform_name() . '"');
header('WWW-Authenticate: ' . $method . ' realm="' . System::get_project_name() . '"');
header('HTTP/1.0 401 Unauthorized');
echo( t('This api method requires authentication'));
echo( t('This API method requires authentication.'));
killme();
}

View file

@ -43,15 +43,10 @@ function account_verify_password($login, $pass)
{
$ret = [ 'account' => null, 'channel' => null, 'xchan' => null ];
$login = punify($login);
$email_verify = get_config('system', 'verify_email');
$register_policy = get_config('system', 'register_policy');
if (! $login) {
return null;
}
$account = null;
$channel = null;
$xchan = null;
@ -76,7 +71,13 @@ function account_verify_password($login, $pass)
if (($addon_auth['authenticated']) && is_array($addon_auth['user_record']) && (! empty($addon_auth['user_record']))) {
$ret['account'] = $addon_auth['user_record'];
return $ret;
} else {
}
else {
if (! $login) {
logger('No login identity provided or authenticate addon failed.');
return false;
}
$login = punify($login);
if (! strpos($login, '@')) {
$channel = Channel::from_username($login);
if (! $channel) {
@ -230,7 +231,7 @@ if (
App::$session->new_cookie(60 * 60 * 24); // one day
$_SESSION['last_login_date'] = datetime_convert();
unset($_SESSION['visitor_id']); // no longer a visitor
authenticate_success($x[0], null, true, true);
authenticate_success($x[0], false, true, true);
}
}
if (array_key_exists('atoken', $_SESSION)) {
@ -279,7 +280,7 @@ if (
$login_refresh = true;
}
$ch = (($_SESSION['uid']) ? Channel::from_id($_SESSION['uid']) : null);
authenticate_success($r[0], null, $ch, false, false, $login_refresh);
authenticate_success($r[0], false, $ch, false, false, $login_refresh);
} else {
$_SESSION['account_id'] = 0;
App::$session->nuke();

View file

@ -17,7 +17,7 @@ use Code\Extend\Hook;
* @param bool $return
* @param bool $update_lastlog
*/
function authenticate_success($user_record, $channel = null, $login_initial = false, $interactive = false, $return = false, $update_lastlog = false)
function authenticate_success($user_record, $channel = false, $login_initial = false, $interactive = false, $return = false, $update_lastlog = false)
{
$_SESSION['addr'] = $_SERVER['REMOTE_ADDR'];

View file

@ -948,7 +948,7 @@ function get_tags($s)
// Pull out single word tags. These can be @nickname, @first_last
// and #hash tags.
if (preg_match_all('/(?<![a-zA-Z0-9=\pL\/\?\;])([@#]\!?[^ \x0D\x0A,;:\?\[\{\&]+)/u', $s, $match)) {
if (preg_match_all('/(?<![a-zA-Z0-9=\pL\/\?\;\#])([@#]\!?[^ \x0D\x0A,;:\?\[\{\&]+)/u', $s, $match)) {
foreach ($match[1] as $mtch) {
// Cleanup/ignore false positives

View file

@ -31,7 +31,7 @@ cli_startup();
list($tmp, $id) = array_map('trim', explode('/', $file));
$info = Addon::get_info($id);
$enabled = in_array($id,$installed);
$x = check_plugin_versions($info);
$x = Addon::check_versions($info);
if($enabled && ! $x) {
$enabled = false;
Addon::uninstall($id);

View file

@ -1,2 +1,2 @@
<?php
define ( 'STD_VERSION', '22.03.12' );
define ( 'STD_VERSION', '22.03.15' );

View file

@ -1,7 +1,7 @@
<?php
use Code\Lib\Channel;
if(! App::$install) {
if (! App::$install) {
// Get the UID of the channel owner
$uid = Channel::get_theme_uid();
@ -36,9 +36,9 @@ if(! App::$install) {
// Setting $schema to '' wasn't working for some reason, so we'll check it's
// not --- like the mobile theme does instead.
// Allow layouts to over-ride the schema
// Allow layouts to over-ride the schema - used as a filename component so sanitize.
$schema = ((isset($_REQUEST['schema']) && $_REQUEST['schema']) ? $_REQUEST['schema'] : EMPTY_STR);
$schema = str_replace(['/', '.'], [ '', '' ], ((isset($_REQUEST['schema']) && $_REQUEST['schema']) ? $_REQUEST['schema'] : EMPTY_STR));
if (($schema) && ($schema != '---')) {