Commit 4c5f0a05 authored by Mario's avatar Mario
Browse files

Merge branch '6.2RC'

parents 95240736 977a55eb
Hubzilla 6.2 (2021-09-08)
- Deprecate the custom highlight [hl] bbcode in favor of [mark] which is a html5 standard
- Check post_mail permission when receiving a direct message
- Refactor actor store and cache mechanism
- Add optional strict flag (false) to base64url_decode() which is passed on to base64_decode()
- Update russian translations
- Add optional force flag (false) to Activity::actor_store()
- Improved icon lookup for actor objects
- Improved desktop notifications
- Make Hubzilla installable as PWA in supported browsers
- Capitalized widget titles (redbasic)
- Make images loaded counter showing % loaded instead of actual image count
- Deprecate optional channel menu in favor of extended app bin
- Implement infrastructure to provide an app install widget for modules
- Implement app descriptions
- Implement app drag and drop sorting
- Implement app drag and drop pinning/starring
- Update polish translations
- Update to bootstrap 5 and adjust templates
Bugfixes
- Fix anonymous comments for cards and articles
- Resolve regex interference between contact and channel autocomplete
- Fix files stored in wrong directory when uploaded in the photo module
- Fix records with empty public key selected for OWA
- Fix import_author_zot failing if primary is both dead and unknown - issue #1599
- Fix regression with bookmarks
- Fix pubstream notifications link redirecting to mod hq when not local
- Cleanup legacy mail leftovers - issue #1595
- Fix getimagesize() called with empty path
- Fix display issue caused by redundant closing div tag
Addons
- Pubcrawl: fix post to forum - issue #159
- Diaspora: implement post_mail permissions for direct messages
- Pubcrawl: implement post_mail permissions for direct messages
- Pubcrawl: deprecate asencode_person() in favor of Activity::encode_person()
- Pubcrawl: implement Activity::get_actor_collections() to reflect core actor store changes
- Diaspora: fix use of deprecated function
- Twitter: prevent re-retweeting of a post from a clone if it has already been sent
- Totp: use platform name instead of banner text
- Pubcrawl: use force flag when updating an actor
- Sse: do not process possible empty hashes
- Implement app descriptions for addons that already support the apps infrastructure
Hubzilla 6.0.1 (2021-07-27)
- Fix regression in notification handling introduced in 6.0
- Fix regression in regard to unified session page load times introduced in 6.0
......
// This file should be served from the web root to avoid scope and cookie related issues with some browsers
self.addEventListener('install', function(e) {
console.log('install event');
});
self.addEventListener('fetch', function(e) {
// nothing here yet
return;
});
......@@ -65,9 +65,9 @@ class Permissions {
'write_wiki' => t('Can write to my wiki pages'),
'post_wall' => t('Can post on my channel (wall) page'),
'post_comments' => t('Can comment on or like my posts'),
'post_mail' => t('Can send me private mail messages'),
'post_mail' => t('Can send me direct messages'),
'post_like' => t('Can like/dislike profiles and profile things'),
'tag_deliver' => t('Can forward to all my channel connections via ! mentions in posts'),
'tag_deliver' => t('Can forward direct messages to all my channel connections (forum)'),
'chat' => t('Can chat with me'),
'republish' => t('Can source my public posts in derived channels'),
'delegate' => t('Can administer my channel')
......
......@@ -48,13 +48,6 @@ class Cron {
db_quoteinterval('3 MINUTE')
);
// expire any expired mail
q("delete from mail where expires > '%s' and expires < %s ",
dbesc(NULL_DATE),
db_utcnow()
);
require_once('include/account.php');
remove_expired_registrations();
......
......@@ -126,7 +126,10 @@ class Onepoll {
$max = intval(get_config('system', 'max_imported_posts', 30));
if (intval($max)) {
$cl = get_xconfig($contact['abook_xchan'], 'activitypub', 'collections');
$cl = Activity::get_actor_collections($contact['abook_xchan']);
if(empty($cl)) {
$cl = get_xconfig($contact['abook_xchan'], 'activitypub', 'collections');
}
if (is_array($cl) && array_key_exists('outbox', $cl)) {
$url = $cl['outbox'];
......
......@@ -8,6 +8,8 @@ use Zotlabs\Access\PermissionRoles;
use Zotlabs\Access\Permissions;
use Zotlabs\Daemon\Master;
use Zotlabs\Web\HTTPSig;
use Zotlabs\Lib\XConfig;
use Zotlabs\Lib\Libzot;
require_once('include/event.php');
require_once('include/html2plain.php');
......@@ -590,13 +592,11 @@ class Activity {
break;
case 'Mention':
$mention_type = substr($t['name'], 0, 1);
if ($mention_type === '!') {
$ret[] = ['ttype' => TERM_FORUM, 'url' => $t['href'], 'term' => escape_tags(substr($t['name'], 1))];
}
else {
$ret[] = ['ttype' => TERM_MENTION, 'url' => $t['href'], 'term' => escape_tags((substr($t['name'], 0, 1) === '@') ? substr($t['name'], 1) : $t['name'])];
}
$ret[] = ['ttype' => TERM_MENTION, 'url' => $t['href'], 'term' => escape_tags((substr($t['name'], 0, 1) === '@') ? substr($t['name'], 1) : $t['name'])];
break;
case 'Bookmark':
$ret[] = ['ttype' => TERM_BOOKMARK, 'url' => $t['href'], 'term' => escape_tags($t['name'])];
break;
default:
......@@ -623,14 +623,14 @@ class Activity {
}
break;
case TERM_FORUM:
$ret[] = ['type' => 'Mention', 'href' => $t['url'], 'name' => '!' . $t['term']];
break;
case TERM_MENTION:
$ret[] = ['type' => 'Mention', 'href' => $t['url'], 'name' => '@' . $t['term']];
break;
case TERM_BOOKMARK:
$ret[] = ['type' => 'Bookmark', 'href' => $t['url'], 'name' => $t['term']];
break;
default:
break;
}
......@@ -1111,6 +1111,7 @@ class Activity {
call_hooks('encode_person', $arr);
$ret = $arr['encoded'];
return $ret;
}
......@@ -1533,12 +1534,55 @@ class Activity {
return;
}
static function actor_store($url, $person_obj) {
static function actor_store($url, $person_obj, $force = false) {
if (!is_array($person_obj)) {
return;
}
/* not implemented
if (array_key_exists('movedTo',$person_obj) && $person_obj['movedTo'] && ! is_array($person_obj['movedTo'])) {
$tgt = self::fetch($person_obj['movedTo']);
if (is_array($tgt)) {
self::actor_store($person_obj['movedTo'],$tgt);
ActivityPub::move($person_obj['id'],$tgt);
}
return;
}
*/
$ap_hubloc = null;
$hublocs = self::get_actor_hublocs($url);
if ($hublocs) {
foreach ($hublocs as $hub) {
if ($hub['hubloc_network'] === 'activitypub') {
$ap_hubloc = $hub;
}
if ($hub['hubloc_network'] === 'zot6') {
Libzot::update_cached_hubloc($hub);
}
}
}
if ($ap_hubloc) {
// we already have a stored record. Determine if it needs updating.
if ($ap_hubloc['hubloc_updated'] < datetime_convert('UTC','UTC',' now - 3 days') || $force) {
$person_obj = self::fetch($url);
}
else {
return;
}
}
if (isset($person_obj['id'])) {
$url = $person_obj['id'];
}
if (! $url) {
return;
}
$inbox = $person_obj['inbox'];
// invalid identity
......@@ -1547,6 +1591,9 @@ class Activity {
return;
}
// store the actor record in XConfig
XConfig::Set($url, 'system', 'actor_record', $person_obj);
$name = $person_obj['name'];
if (!$name) {
$name = $person_obj['preferredUsername'];
......@@ -1558,13 +1605,21 @@ class Activity {
$icon = z_root() . '/' . get_default_profile_photo(300);
if ($person_obj['icon']) {
if (is_array($person_obj['icon'])) {
if (array_key_exists('url', $person_obj['icon']))
if (array_key_exists('url', $person_obj['icon'])) {
$icon = $person_obj['icon']['url'];
else
$icon = $person_obj['icon'][0]['url'];
}
else {
if (is_string($person_obj['icon'][0])) {
$icon = $person_obj['icon'][0];
}
elseif (array_key_exists('url', $person_obj['icon'][0])) {
$icon = $person_obj['icon'][0]['url'];
}
}
}
else
else {
$icon = $person_obj['icon'];
}
}
$links = false;
......@@ -1597,20 +1652,6 @@ class Activity {
$profile = $url;
}
$collections = [];
if ($inbox) {
$collections['inbox'] = $inbox;
if (array_key_exists('outbox', $person_obj))
$collections['outbox'] = $person_obj['outbox'];
if (array_key_exists('followers', $person_obj))
$collections['followers'] = $person_obj['followers'];
if (array_key_exists('following', $person_obj))
$collections['following'] = $person_obj['following'];
if (array_key_exists('endpoints', $person_obj) && array_key_exists('sharedInbox', $person_obj['endpoints']))
$collections['sharedInbox'] = $person_obj['endpoints']['sharedInbox'];
}
if (array_key_exists('publicKey', $person_obj) && array_key_exists('publicKeyPem', $person_obj['publicKey'])) {
if ($person_obj['id'] === $person_obj['publicKey']['owner']) {
$pubkey = $person_obj['publicKey']['publicKeyPem'];
......@@ -1635,7 +1676,7 @@ class Activity {
// Record exists. Cache existing records for one week at most
// then refetch to catch updated profile photos, names, etc.
$d = datetime_convert('UTC', 'UTC', 'now - 3 days');
if($r[0]['hubloc_updated'] > $d) {
if($r[0]['hubloc_updated'] > $d && !$force) {
return;
}
......@@ -1696,10 +1737,6 @@ class Activity {
);
}
if ($collections) {
set_xconfig($url, 'activitypub', 'collections', $collections);
}
$photos = import_xchan_photo($icon, $url);
q("update xchan set xchan_photo_date = '%s', xchan_photo_l = '%s', xchan_photo_m = '%s', xchan_photo_s = '%s', xchan_photo_mimetype = '%s' where xchan_hash = '%s'",
dbescdate(datetime_convert('UTC', 'UTC', $photos[5])),
......@@ -1778,12 +1815,27 @@ class Activity {
}
else {
if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') && !$is_sys_channel) {
logger('no permission');
logger('no send_stream permission');
return;
}
$s['owner_xchan'] = $s['author_xchan'] = $observer_hash;
}
if ($act->recips && (!in_array(ACTIVITY_PUBLIC_INBOX, $act->recips)))
$s['item_private'] = 1;
if (array_key_exists('directMessage', $act->obj) && intval($act->obj['directMessage'])) {
$s['item_private'] = 2;
}
if (intval($s['item_private']) === 2) {
if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'post_mail')) {
logger('no post_mail permission');
return;
}
}
$abook = q("select * from abook where abook_xchan = '%s' and abook_channel = %d limit 1",
dbesc($observer_hash),
intval($channel['channel_id'])
......@@ -1932,14 +1984,6 @@ class Activity {
}
}
if ($act->recips && (!in_array(ACTIVITY_PUBLIC_INBOX, $act->recips)))
$s['item_private'] = 1;
if (array_key_exists('directMessage', $act->obj) && intval($act->obj['directMessage'])) {
$s['item_private'] = 2;
}
set_iconfig($s, 'activitypub', 'recips', $act->raw_recips);
if ($parent) {
set_iconfig($s, 'activitypub', 'rawmsg', $act->raw, 1);
......@@ -2635,6 +2679,11 @@ class Activity {
$allowed = true;
}
if (intval($item['item_private']) === 2) {
if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'post_mail')) {
$allowed = false;
}
}
if ($is_sys_channel) {
......@@ -3498,4 +3547,53 @@ class Activity {
}
static function get_cached_actor($id) {
return (XConfig::Get($id,'system','actor_record'));
}
static function get_actor_hublocs($url, $options = 'all') {
$hublocs = false;
switch ($options) {
case 'activitypub':
$hublocs = q("select * from hubloc left join xchan on hubloc_hash = xchan_hash where hubloc_hash = '%s' and hubloc_deleted = 0 ",
dbesc($url)
);
break;
case 'zot6':
$hublocs = q("select * from hubloc left join xchan on hubloc_hash = xchan_hash where hubloc_id_url = '%s' and hubloc_deleted = 0 ",
dbesc($url)
);
break;
case 'all':
default:
$hublocs = q("select * from hubloc left join xchan on hubloc_hash = xchan_hash where ( hubloc_id_url = '%s' OR hubloc_hash = '%s' ) and hubloc_deleted = 0 ",
dbesc($url),
dbesc($url)
);
break;
}
return $hublocs;
}
static function get_actor_collections($url) {
$ret = [];
$actor_record = XConfig::Get($url,'system','actor_record');
if (! $actor_record) {
return $ret;
}
foreach ( [ 'inbox','outbox','followers','following' ] as $collection) {
if (isset($actor_record[$collection]) && $actor_record[$collection]) {
$ret[$collection] = $actor_record[$collection];
}
}
if (array_path_exists('endpoints/sharedInbox',$actor_record) && $actor_record['endpoints']['sharedInbox']) {
$ret['sharedInbox'] = $actor_record['endpoints']['sharedInbox'];
}
return $ret;
}
}
......@@ -300,17 +300,8 @@ class ActivityStreams {
function get_actor($property, $base = '', $namespace = '') {
$x = $this->get_property_obj($property, $base, $namespace);
if ($this->is_url($x)) {
// SECURITY: If we have already stored the actor profile, re-generate it
// from cached data - don't refetch it from the network
$r = q("select * from xchan join hubloc on xchan_hash = hubloc_hash where hubloc_id_url = '%s'",
dbesc($x)
);
if ($r) {
$r = Libzot::zot_record_preferred($r);
$y = Activity::encode_person($r);
$y['cached'] = true;
$y = Activity::get_cached_actor($x);
if ($y) {
return $y;
}
}
......
......@@ -2,6 +2,7 @@
namespace Zotlabs\Lib;
use App;
use Zotlabs\Lib\Libsync;
require_once('include/plugin.php');
......@@ -77,7 +78,8 @@ class Apps {
'Search',
'Help',
'Profile Photo',
'HQ'
'HQ',
'Post'
]);
/**
......@@ -537,13 +539,47 @@ class Apps {
$install_action = (($installed) ? t('Update') : t('Install'));
$icon = ((strpos($papp['photo'],'icon:') === 0) ? substr($papp['photo'],5) : '');
if (!$installed && $mode === 'module') {
$_SESSION['return_url'] = App::$query_string;
return replace_macros(get_markup_template('app_install.tpl'), [
'$papp' => $papp,
'$install' => $install_action
]);
}
if($mode === 'navbar') {
return replace_macros(get_markup_template('app_nav_pinned.tpl'),array(
'$app' => $papp,
'$icon' => $icon,
));
}
if($mode === 'nav') {
return replace_macros(get_markup_template('app_nav.tpl'),array(
'$app' => $papp,
'$icon' => $icon,
));
}
if($mode === 'inline') {
return replace_macros(get_markup_template('app_inline.tpl'),array(
'$app' => $papp,
'$icon' => $icon,
'$installed' => $installed,
'$purchase' => ((isset($papp['page']) && (! $installed)) ? t('Purchase') : ''),
'$action_label' => $install_action
));
}
if(in_array($mode, ['nav-order', 'nav-order-pinned'])) {
return replace_macros(get_markup_template('app_order.tpl'),array(
'$app' => $papp,
'$icon' => $icon,
'$hosturl' => $hosturl,
'$mode' => $mode
));
}
if($mode === 'install') {
$papp['embed'] = true;
}
......@@ -564,8 +600,6 @@ class Apps {
'$pin' => ((isset($papp['embed']) || $mode == 'edit') ? false : true),
'$featured' => ((strpos($papp['categories'], 'nav_featured_app') === false) ? false : true),
'$pinned' => ((strpos($papp['categories'], 'nav_pinned_app') === false) ? false : true),
'$navapps' => (($mode == 'nav') ? true : false),
'$order' => (($mode === 'nav-order' || $mode === 'nav-order-pinned') ? true : false),
'$mode' => $mode,
'$add' => t('Add to app-tray'),
'$remove' => t('Remove from app-tray'),
......@@ -575,6 +609,7 @@ class Apps {
));
}
static public function app_install($uid,$app) {
if(! is_array($app)) {
......@@ -1357,4 +1392,17 @@ class Apps {
return chunk_split(base64_encode(json_encode($papp)),72,"\n");
}
static public function get_papp($app) {
$r = q("select * from app where app_id = '%s' and app_channel = 0 limit 1",
dbesc(hash('whirlpool', $app))
);
if ($r) {
$papp = self::app_encode($r[0]);
return $papp;
}
return false;
}
}
......@@ -854,8 +854,8 @@ class Enotify {
'photo' => $item[$who]['xchan_photo_s'],
'when' => (($edit) ? datetime_convert('UTC', date_default_timezone_get(), $item['edited']) : datetime_convert('UTC', date_default_timezone_get(), $item['created'])),
'class' => (intval($item['item_unseen']) ? 'notify-unseen' : 'notify-seen'),
'b64mid' => (($item['mid']) ? 'b64.' . base64url_encode($item['mid']) : ''),
//'b64mid' => ((in_array($item['verb'], [ACTIVITY_LIKE, ACTIVITY_DISLIKE])) ? 'b64.' . base64url_encode($item['thr_parent']) : 'b64.' . base64url_encode($item['mid'])),
'b64mid' => (($item['mid']) ? gen_link_id($item['mid']) : ''),
//'b64mid' => ((in_array($item['verb'], [ACTIVITY_LIKE, ACTIVITY_DISLIKE])) ? gen_link_id($item['thr_parent']) : gen_link_id($item['mid'])),
'thread_top' => (($item['item_thread_top']) ? true : false),
'message' => bbcode(escape_tags($itemem_text)),
'body' => htmlentities(html2plain(bbcode($item['body'], ['drop_media', true]), 75, true), ENT_QUOTES, 'UTF-8', false),
......@@ -882,7 +882,7 @@ class Enotify {
$mid = basename($tt['link']);
$b64mid = ((strpos($mid, 'b64.') === 0) ? $mid : 'b64.' . base64url_encode($mid));
$b64mid = gen_link_id($mid);
$x = [
'notify_link' => (($tt['ntype'] === NOTIFY_MAIL) ? $tt['link'] : z_root() . '/notify/view/' . $tt['id']),
'name' => $tt['xname'],
......@@ -910,7 +910,7 @@ class Enotify {
'when' => datetime_convert('UTC', date_default_timezone_get(), $rr['abook_created']),
'hclass' => ('notify-unseen'),
'message' => t('added your channel')
];
];
}
......
......@@ -1601,6 +1601,12 @@ class Libzot {
$friendofriend = true;
}
if (intval($arr['item_private']) === 2) {
if (!perm_is_allowed($channel['channel_id'], $sender, 'post_mail')) {
$allowed = false;
}
}
if (!$allowed) {
logger("permission denied for delivery to channel {$channel['channel_id']} {$channel['channel_address']}");
$DR->update('permission denied');
......@@ -1910,16 +1916,18 @@ class Libzot {
// logger($AS->debug());
$r = q("select hubloc_hash from hubloc where hubloc_id_url = '%s' limit 1",
$r = q("select hubloc_hash, hubloc_network from hubloc where hubloc_id_url = '%s'",
dbesc($AS->actor['id'])
);
$r = self::zot_record_preferred($r);
if (!$r) {
$y = import_author_xchan(['url' => $AS->actor['id']]);
if ($y) {
$r = q("select hubloc_hash from hubloc where hubloc_id_url = '%s' limit 1",
$r = q("select hubloc_hash, hubloc_network from hubloc where hubloc_id_url = '%s'",
dbesc($AS->actor['id'])
);
$r = self::zot_record_preferred($r);
}
if (!$r) {
logger('FOF Activity: no actor');
......@@ -1935,9 +1943,8 @@ class Libzot {
}
}
if ($r) {
$arr['author_xchan'] = $r[0]['hubloc_hash'];
$arr['author_xchan'] = $r['hubloc_hash'];
}
if ($signer) {
......@@ -2583,21 +2590,26 @@ class Libzot {
$desturl = $x['url'];
$found_primary = false;
$r1 = q("select hubloc_url, hubloc_updated, site_dead from hubloc left join site on
hubloc_url = site_url where hubloc_guid = '%s' and hubloc_guid_sig = '%s' and hubloc_primary = 1 limit 1",
dbesc($x['id']),
dbesc($x['id_sig'])
);
if ($r1) {
$found_primary = true;
}
$r2 = q("select xchan_hash from xchan where xchan_guid = '%s' and xchan_guid_sig = '%s' limit 1",
dbesc($x['id']),
dbesc($x['id_sig'])
);
$site_dead = false;
$primary_dead = false;
if ($r1 && intval($r1[0]['site_dead'])) {
$site_dead = true;
$primary_dead = true;
}
// We have valid and somewhat fresh information. Always true if it is our own site.
......@@ -2615,13 +2627,14 @@ class Libzot {