diff options
Diffstat (limited to 'Zotlabs/Lib')
-rw-r--r-- | Zotlabs/Lib/AccessList.php | 236 | ||||
-rw-r--r-- | Zotlabs/Lib/Activity.php | 1041 | ||||
-rw-r--r-- | Zotlabs/Lib/ActivityStreams.php | 41 | ||||
-rw-r--r-- | Zotlabs/Lib/Apps.php | 179 | ||||
-rw-r--r-- | Zotlabs/Lib/Connect.php | 7 | ||||
-rw-r--r-- | Zotlabs/Lib/Crypto.php | 4 | ||||
-rw-r--r-- | Zotlabs/Lib/DReport.php | 3 | ||||
-rw-r--r-- | Zotlabs/Lib/Enotify.php | 145 | ||||
-rw-r--r-- | Zotlabs/Lib/Group.php | 405 | ||||
-rw-r--r-- | Zotlabs/Lib/LDSignatures.php | 11 | ||||
-rw-r--r-- | Zotlabs/Lib/Libsync.php | 110 | ||||
-rw-r--r-- | Zotlabs/Lib/Libzot.php | 429 | ||||
-rw-r--r-- | Zotlabs/Lib/Libzotdir.php | 48 | ||||
-rw-r--r-- | Zotlabs/Lib/MessageFilter.php | 216 | ||||
-rw-r--r-- | Zotlabs/Lib/NativeWiki.php | 48 | ||||
-rw-r--r-- | Zotlabs/Lib/NativeWikiPage.php | 579 | ||||
-rw-r--r-- | Zotlabs/Lib/PConfig.php | 85 | ||||
-rw-r--r-- | Zotlabs/Lib/Permcat.php | 173 | ||||
-rw-r--r-- | Zotlabs/Lib/Queue.php | 94 | ||||
-rw-r--r-- | Zotlabs/Lib/Share.php | 42 | ||||
-rw-r--r-- | Zotlabs/Lib/ThreadItem.php | 54 | ||||
-rw-r--r-- | Zotlabs/Lib/ThreadStream.php | 8 | ||||
-rw-r--r-- | Zotlabs/Lib/ZotURL.php | 2 | ||||
-rw-r--r-- | Zotlabs/Lib/Zotfinger.php | 26 |
24 files changed, 2200 insertions, 1786 deletions
diff --git a/Zotlabs/Lib/AccessList.php b/Zotlabs/Lib/AccessList.php index 3c008f8c7..f4b762eaa 100644 --- a/Zotlabs/Lib/AccessList.php +++ b/Zotlabs/Lib/AccessList.php @@ -1,38 +1,37 @@ -<?php +<?php namespace Zotlabs\Lib; -use Zotlabs\Lib\Libsync; - - class AccessList { - - static function add($uid,$name,$public = 0) { - $ret = false; + static function add($uid, $name, $public = 0) { + + $ret = false; + $hash = ''; if ($uid && $name) { - $r = self::byname($uid,$name); // check for dups + $r = self::by_name($uid, $name); // check for dups if ($r !== false) { - // This could be a problem. + // This could be a problem. // Let's assume we've just created a list which we once deleted // all the old members are gone, but the list remains so we don't break any security // access lists. What we're doing here is reviving the dead list, but old content which - // was restricted to this list may now be seen by the new list members. + // was restricted to this list may now be seen by the new list members. $z = q("SELECT * FROM pgrp WHERE id = %d LIMIT 1", intval($r) ); - if(($z) && $z[0]['deleted']) { + if (($z) && $z[0]['deleted']) { q('UPDATE pgrp SET deleted = 0 WHERE id = %d', intval($z[0]['id'])); - notice( t('A deleted list with this name was revived. Existing item permissions <strong>may</strong> apply to this list and any future members. If this is not what you intended, please create another list with a different name.') . EOL); + notice(t('A deleted privacy group with this name was revived. Existing item permissions <strong>may</strong> apply to this privacy group and any future members. If this is not what you intended, please create another privacy group with a different name.') . EOL); } - return true; + $hash = self::by_id($uid, $r); + return $hash; } $hash = new_uuid(); - $r = q("INSERT INTO pgrp ( hash, uid, visible, gname ) + $r = q("INSERT INTO pgrp ( hash, uid, visible, gname ) VALUES( '%s', %d, %d, '%s' ) ", dbesc($hash), intval($uid), @@ -42,12 +41,12 @@ class AccessList { $ret = $r; } - Libsync::build_sync_packet($uid,null,true); - return $ret; - } + Libsync::build_sync_packet($uid, null, true); + return (($ret) ? $hash : $ret); + } - static function remove($uid,$name) { + static function remove($uid, $name) { $ret = false; if ($uid && $name) { $r = q("SELECT id, hash FROM pgrp WHERE uid = %d AND gname = '%s' LIMIT 1", @@ -55,36 +54,36 @@ class AccessList { dbesc($name) ); if ($r) { - $group_id = $r[0]['id']; + $group_id = $r[0]['id']; $group_hash = $r[0]['hash']; } else { return false; } - + // remove group from default posting lists $r = q("SELECT channel_default_group, channel_allow_gid, channel_deny_gid FROM channel WHERE channel_id = %d LIMIT 1", - intval($uid) + intval($uid) ); if ($r) { $user_info = array_shift($r); - $change = false; + $change = false; if ($user_info['channel_default_group'] == $group_hash) { $user_info['channel_default_group'] = ''; - $change = true; + $change = true; } if (strpos($user_info['channel_allow_gid'], '<' . $group_hash . '>') !== false) { $user_info['channel_allow_gid'] = str_replace('<' . $group_hash . '>', '', $user_info['channel_allow_gid']); - $change = true; + $change = true; } if (strpos($user_info['channel_deny_gid'], '<' . $group_hash . '>') !== false) { $user_info['channel_deny_gid'] = str_replace('<' . $group_hash . '>', '', $user_info['channel_deny_gid']); - $change = true; + $change = true; } if ($change) { - q("UPDATE channel SET channel_default_group = '%s', channel_allow_gid = '%s', channel_deny_gid = '%s' + q("UPDATE channel SET channel_default_group = '%s', channel_allow_gid = '%s', channel_deny_gid = '%s' WHERE channel_id = %d", intval($user_info['channel_default_group']), dbesc($user_info['channel_allow_gid']), @@ -110,16 +109,16 @@ class AccessList { } - Libsync::build_sync_packet($uid,null,true); + Libsync::build_sync_packet($uid, null, true); return $ret; } // returns the integer id of an access group owned by $uid and named $name // or false. - - static function byname($uid,$name) { - if (! ($uid && $name)) { + + static function by_name($uid, $name) { + if (!($uid && $name)) { return false; } $r = q("SELECT id FROM pgrp WHERE uid = %d AND gname = '%s' LIMIT 1", @@ -132,11 +131,11 @@ class AccessList { return false; } - static function by_id($uid,$id) { - if (! ($uid && $id)) { + static function by_id($uid, $id) { + if (!($uid && $id)) { return false; } - + $r = q("SELECT * FROM pgrp WHERE uid = %d AND id = %d and deleted = 0", intval($uid), intval($id) @@ -147,10 +146,8 @@ class AccessList { return false; } - - - static function rec_byhash($uid,$hash) { - if (! ( $uid && $hash)) { + static function by_hash($uid, $hash) { + if (!($uid && $hash)) { return false; } $r = q("SELECT * FROM pgrp WHERE uid = %d AND hash = '%s' LIMIT 1", @@ -163,46 +160,46 @@ class AccessList { return false; } - - static function member_remove($uid,$name,$member) { - $gid = self::byname($uid,$name); - if (! $gid) { - return false; + static function member_remove($uid, $name, $member, $gid = 0) { + if (!$gid) { + $gid = self::by_name($uid, $name); } - if (! ($uid && $gid && $member)) { + + if (!($uid && $gid && $member)) { return false; } + $r = q("DELETE FROM pgrp_member WHERE uid = %d AND gid = %d AND xchan = '%s' ", intval($uid), intval($gid), dbesc($member) ); - Libsync::build_sync_packet($uid,null,true); + Libsync::build_sync_packet($uid, null, true); return $r; } - - static function member_add($uid,$name,$member,$gid = 0) { - if (! $gid) { - $gid = self::byname($uid,$name); + static function member_add($uid, $name, $member, $gid = 0) { + if (!$gid) { + $gid = self::by_name($uid, $name); } - if (! ($gid && $uid && $member)) { + if (!($gid && $uid && $member)) { return false; } - $r = q("SELECT * FROM pgrp_member WHERE uid = %d AND gid = %d AND xchan = '%s' LIMIT 1", + $r = q("SELECT * FROM pgrp_member WHERE uid = %d AND gid = %d AND xchan = '%s' LIMIT 1", intval($uid), intval($gid), dbesc($member) ); if ($r) { - return true; // You might question this, but - // we indicate success because the group member was in fact created - // -- It was just created at another time + return true; + // You might question this, but + // we indicate success because the group member was in fact created + // -- It was just created at another time } - else { + else { $r = q("INSERT INTO pgrp_member (uid, gid, xchan) VALUES( %d, %d, '%s' ) ", intval($uid), @@ -210,15 +207,14 @@ class AccessList { dbesc($member) ); } - Libsync::build_sync_packet($uid,null,true); + Libsync::build_sync_packet($uid, null, true); return $r; } - static function members($uid, $gid) { $ret = []; if (intval($gid)) { - $r = q("SELECT * FROM pgrp_member + $r = q("SELECT * FROM pgrp_member LEFT JOIN abook ON abook_xchan = pgrp_member.xchan left join xchan on xchan_hash = abook_xchan WHERE gid = %d AND abook_channel = %d and pgrp_member.uid = %d and xchan_deleted = 0 and abook_self = 0 and abook_blocked = 0 and abook_pending = 0 ORDER BY xchan_name ASC ", intval($gid), @@ -232,7 +228,7 @@ class AccessList { return $ret; } - static function members_xchan($uid,$gid) { + static function members_xchan($uid, $gid) { $ret = []; if (intval($gid)) { $r = q("SELECT xchan FROM pgrp_member WHERE gid = %d AND uid = %d", @@ -248,99 +244,75 @@ class AccessList { return $ret; } - static function members_profile_xchan($uid,$gid) { + static function profile_members_xchan($uid,$gid) { $ret = []; - if (intval($gid)) { + + if(intval($gid)) { $r = q("SELECT abook_xchan as xchan from abook left join profile on abook_profile = profile_guid where profile.id = %d and profile.uid = %d", intval($gid), intval($uid) ); - if ($r) { - foreach($r as $rv) { - $ret[] = $rv['xchan']; + if($r) { + foreach($r as $rr) { + $ret[] = $rr['xchan']; } } } return $ret; } + static function select($uid, $options) { + $selected = $options['selected'] ?? ''; + $form_id = $options['form_id'] ?? 'accesslist_select'; + $label = $options['label'] ?? t('Select a privacy group'); + $before = $options['before'] ?? []; + $after = $options['after'] ?? []; - - static function select($uid,$group = '') { - $grps = []; + $o = ''; - $r = q("SELECT * FROM pgrp WHERE deleted = 0 AND uid = %d ORDER BY gname ASC", - intval($uid) - ); - $grps[] = [ 'name' => '', 'hash' => '0', 'selected' => '' ]; - if ($r) { - foreach ($r as $rr) { - $grps[] = [ 'name' => $rr['gname'], 'id' => $rr['hash'], 'selected' => (($group == $rr['hash']) ? 'true' : '') ]; - } + $grps[] = [ + 'name' => '', + 'id' => '0', + 'selected' => false + ]; + if ($before) { + $grps[] = $before; } - - return replace_macros(get_markup_template('group_selection.tpl'), [ - '$label' => t('Add new connections to this access list'), - '$groups' => $grps - ]); - } - - - static function widget($every="connections",$each="lists",$edit = false, $group_id = 0, $cid = '',$mode = 1) { - - $o = ''; - - $groups = []; $r = q("SELECT * FROM pgrp WHERE deleted = 0 AND uid = %d ORDER BY gname ASC", - intval($_SESSION['uid']) + intval($uid) ); - $member_of = []; - if ($cid) { - $member_of = self::containing(local_channel(),$cid); - } - if ($r) { - foreach ($r as $rr) { - $selected = (($group_id == $rr['id']) ? ' group-selected' : ''); - - if ($edit) { - $groupedit = [ 'href' => "lists/".$rr['id'], 'title' => t('edit') ]; - } - else { - $groupedit = null; - } - - $groups[] = [ - 'id' => $rr['id'], - 'enc_cid' => base64url_encode($cid), - 'cid' => $cid, - 'text' => $rr['gname'], - 'selected' => $selected, - 'href' => (($mode == 0) ? $each.'?f=&gid='.$rr['id'] : $each."/".$rr['id']) . ((x($_GET,'new')) ? '&new=' . $_GET['new'] : '') . ((x($_GET,'order')) ? '&order=' . $_GET['order'] : ''), - 'edit' => $groupedit, - 'ismember' => in_array($rr['id'],$member_of), + if($r) { + foreach($r as $rr) { + $grps[] = [ + 'name' => $rr['gname'], + 'id' => $rr['hash'], + 'selected' => ($selected == $rr['hash']) ]; } } - - return replace_macros(get_markup_template('group_side.tpl'), [ - '$title' => t('Lists'), - '$edittext' => t('Edit list'), - '$createtext' => t('Create new list'), - '$ungrouped' => (($every === 'contacts') ? t('Channels not in any access list') : ''), - '$groups' => $groups, - '$add' => t('add'), - ]); - } + if ($after) { + $grps[] = $after; + } + + logger('select: ' . print_r($grps,true), LOGGER_DATA); + $o = replace_macros(get_markup_template('group_selection.tpl'), array( + '$label' => $label, + '$form_id' => $form_id, + '$groups' => $grps + )); + + return $o; + } static function expand($g) { - if (! (is_array($g) && count($g))) { + if (!(is_array($g) && count($g))) { return []; } @@ -350,8 +322,8 @@ class AccessList { // private profile linked virtual groups foreach ($g as $gv) { - if (substr($gv,0,3) === 'vp.') { - $profile_hash = substr($gv,3); + if (substr($gv, 0, 3) === 'vp.') { + $profile_hash = substr($gv, 3); if ($profile_hash) { $r = q("select abook_xchan from abook where abook_profile = '%s'", dbesc($profile_hash) @@ -366,10 +338,10 @@ class AccessList { else { $x[] = $gv; } - } + } if ($x) { - stringify_array_elms($x,true); + stringify_array_elms($x, true); $groups = implode(',', $x); if ($groups) { $r = q("SELECT xchan FROM pgrp_member WHERE gid IN ( select id from pgrp where hash in ( $groups ))"); @@ -383,9 +355,8 @@ class AccessList { return $ret; } - static function member_of($c) { - $r = q("SELECT pgrp.gname, pgrp.id FROM pgrp LEFT JOIN pgrp_member ON pgrp_member.gid = pgrp.id + $r = q("SELECT pgrp.gname, pgrp.id FROM pgrp LEFT JOIN pgrp_member ON pgrp_member.gid = pgrp.id WHERE pgrp_member.xchan = '%s' AND pgrp.deleted = 0 ORDER BY pgrp.gname ASC ", dbesc($c) ); @@ -393,7 +364,7 @@ class AccessList { return $r; } - static function containing($uid,$c) { + static function containing($uid, $c) { $r = q("SELECT gid FROM pgrp_member WHERE uid = %d AND pgrp_member.xchan = '%s' ", intval($uid), @@ -405,7 +376,8 @@ class AccessList { foreach ($r as $rv) $ret[] = $rv['gid']; } - + return $ret; } -}
\ No newline at end of file + +} diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php index 6e8344def..0c25605e7 100644 --- a/Zotlabs/Lib/Activity.php +++ b/Zotlabs/Lib/Activity.php @@ -11,6 +11,7 @@ use Zotlabs\Web\HTTPSig; require_once('include/event.php'); require_once('include/html2plain.php'); +require_once('include/items.php'); class Activity { @@ -41,9 +42,6 @@ class Activity { if ($x['type'] === ACTIVITY_OBJ_EVENT) { return self::fetch_event($x); } - if ($x['type'] === ACTIVITY_OBJ_PHOTO) { - return self::fetch_image($x); - } call_hooks('encode_object', $x); } @@ -104,7 +102,7 @@ class Activity { if ($x['success']) { $m = parse_url($url); if ($m) { - $y = [ 'scheme' => $m['scheme'], 'host' => $m['host'] ]; + $y = ['scheme' => $m['scheme'], 'host' => $m['host']]; if (array_key_exists('port', $m)) $y['port'] = $m['port']; $site_url = unparse_url($y); @@ -118,6 +116,11 @@ class Activity { $y = json_decode($x['body'], true); logger('returned: ' . json_encode($y, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), LOGGER_DEBUG); + + if (ActivityStreams::is_an_actor($y['type'])) { + XConfig::Set($y['id'], 'system', 'actor_record', $y); + } + return json_decode($x['body'], true); } else { @@ -193,6 +196,7 @@ class Activity { } static function fetch_image($x) { + $ret = [ 'type' => 'Image', 'id' => $x['id'], @@ -287,21 +291,21 @@ class Activity { 'type' => $type . 'Page', ]; - $numpages = $total / App::$pager['itemspage']; - $lastpage = (($numpages > intval($numpages)) ? intval($numpages) + 1 : $numpages); + $numpages = $total / App::$pager['itemspage']; + $lastpage = (($numpages > intval($numpages)) ? intval($numpages) + 1 : $numpages); $url_parts = parse_url($id); $ret['partOf'] = z_root() . '/' . $url_parts['path']; $extra_query_args = ''; - $query_args = null; - if(isset($url_parts['query'])) { + $query_args = null; + if (isset($url_parts['query'])) { parse_str($url_parts['query'], $query_args); } - if(is_array($query_args)) { + if (is_array($query_args)) { unset($query_args['page']); - foreach($query_args as $k => $v) + foreach ($query_args as $k => $v) $extra_query_args .= '&' . urlencode($k) . '=' . urlencode($v); } @@ -375,11 +379,33 @@ class Activity { return $ret; } - static function encode_item($i) { + static function encode_simple_collection($items, $id, $type, $total = 0, $extra = null) { - $ret = []; + $ret = [ + 'id' => z_root() . '/' . $id, + 'type' => $type, + 'totalItems' => $total, + ]; + + if ($extra) { + $ret = array_merge($ret, $extra); + } + if ($items) { + if ($type === 'OrderedCollection') { + $ret['orderedItems'] = $items; + } + else { + $ret['items'] = $items; + } + } + + return $ret; + } + + static function encode_item($i) { + $ret = []; if ($i['verb'] === ACTIVITY_FRIEND) { // Hubzilla 'make-friend' activity, no direct mapping from AS1 to AS2 - make it a note @@ -475,7 +501,7 @@ class Activity { $ret['attributedTo'] = $i['author']['xchan_url']; - if ($i['id'] != $i['parent']) { + if ($i['mid'] !== $i['parent_mid']) { $ret['inReplyTo'] = ((strpos($i['thr_parent'], 'http') === 0) ? $i['thr_parent'] : z_root() . '/item/' . urlencode($i['thr_parent'])); } @@ -508,6 +534,7 @@ class Activity { $top_level = (($i['mid'] === $i['parent_mid']) ? true : false); if ($public) { + $ret['to'] = [ACTIVITY_PUBLIC_INBOX]; $ret['cc'] = [z_root() . '/followers/' . substr($i['author']['xchan_addr'], 0, strpos($i['author']['xchan_addr'], '@'))]; } @@ -581,23 +608,21 @@ class Activity { $ptr = [$ptr]; } foreach ($ptr as $t) { - if (!array_key_exists('type', $t)) + if (is_array($t) && !array_key_exists('type', $t)) $t['type'] = 'Hashtag'; - if (array_key_exists('href', $t) && array_key_exists('name', $t)) { + if (is_array($t) && array_key_exists('href', $t) && array_key_exists('name', $t)) { switch ($t['type']) { case 'Hashtag': $ret[] = ['ttype' => TERM_HASHTAG, 'url' => $t['href'], 'term' => escape_tags((substr($t['name'], 0, 1) === '#') ? substr($t['name'], 1) : $t['name'])]; 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: @@ -624,14 +649,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; } @@ -641,15 +666,15 @@ class Activity { return $ret; } - static function encode_attachment($item) { + static function encode_attachment($item, $iconfig = false) { $ret = []; - if (array_key_exists('attach', $item)) { + if (!$iconfig && array_key_exists('attach', $item)) { $atts = ((is_array($item['attach'])) ? $item['attach'] : json_decode($item['attach'], true)); if ($atts) { foreach ($atts as $att) { - if (strpos($att['type'], 'image')) { + if (isset($att['type']) && strpos($att['type'], 'image')) { $ret[] = ['type' => 'Image', 'url' => $att['href']]; } else { @@ -658,7 +683,7 @@ class Activity { } } } - if (array_key_exists('iconfig', $item) && is_array($item['iconfig'])) { + if ($iconfig && array_key_exists('iconfig', $item) && is_array($item['iconfig'])) { foreach ($item['iconfig'] as $att) { if ($att['sharing']) { $value = ((is_string($att['v']) && preg_match('|^a:[0-9]+:{.*}$|s', $att['v'])) ? unserialize($att['v']) : $att['v']); @@ -758,7 +783,9 @@ class Activity { if (array_path_exists('object/id', $obj)) { $obj['object'] = $obj['object']['id']; } - unset($obj['cc']); + if (isset($obj['cc'])) { + unset($obj['cc']); + } $obj['to'] = [ACTIVITY_PUBLIC_INBOX]; $ret['object'] = $obj; } @@ -848,7 +875,7 @@ class Activity { } } - if ($i['id'] != $i['parent']) { + if ($i['mid'] !== $i['parent_mid']) { $reply = true; // inReplyTo needs to be set in the activity for followup actions (Like, Dislike, Announce, etc.), @@ -865,10 +892,6 @@ class Activity { else return []; - if (strpos($i['body'], '[/share]') !== false) { - $i['obj'] = null; - } - if ($i['obj']) { if (!is_array($i['obj'])) { $i['obj'] = json_decode($i['obj'], true); @@ -878,8 +901,10 @@ class Activity { } $obj = self::encode_object($i['obj']); - if ($obj) + + if ($obj) { $ret['object'] = $obj; + } else return []; } @@ -911,6 +936,11 @@ class Activity { $ret['tag'] = $t; } + $a = self::encode_attachment($i, true); + if ($a) { + $ret['attachment'] = $a; + } + // addressing madness $public = (($i['item_private']) ? false : true); @@ -1021,7 +1051,7 @@ class Activity { $tmp = expand_acl($i['allow_cid']); $list = stringify_array($tmp, true); if ($list) { - $details = q("select hubloc_id_url from hubloc where hubloc_hash in (" . $list . ") and hubloc_id_url != ''"); + $details = q("select hubloc_id_url from hubloc where hubloc_hash in (" . $list . ") and hubloc_id_url != '' and hubloc_deleted = 0"); if ($details) { foreach ($details as $d) { $ret[] = $d['hubloc_id_url']; @@ -1068,10 +1098,11 @@ class Activity { $ret['type'] = 'Person'; if ($c) { - $role = get_pconfig($c['channel_id'], 'system', 'permissions_role'); - if (strpos($role, 'forum') !== false) { + if (get_pconfig($c['channel_id'], 'system', 'group_actor')) { $ret['type'] = 'Group'; } + + $ret['manuallyApprovesFollowers'] = ((get_pconfig($c['channel_id'], 'system', 'autoperms')) ? false : true); } if ($c) { @@ -1094,16 +1125,53 @@ class Activity { 'height' => 300, 'width' => 300, ]; - $ret['url'] = $p['xchan_url']; + +/* This could be used to distinguish actors by protocol instead of tags, + * array urls are not supported by some AP projects (pixelfed) though. + * + $ret['url'] = [ + [ + 'type' => 'Link', + 'rel' => 'alternate', + 'mediaType' => 'application/x-zot+json', + 'href' => $p['xchan_url'] + ], + [ + 'type' => 'Link', + 'rel' => 'alternate', + 'mediaType' => 'application/activity+json', + 'href' => $p['xchan_url'] + ], + [ + 'type' => 'Link', + 'rel' => 'alternate', // 'me'? + 'mediaType' => 'text/html', + 'href' => $p['xchan_url'] + ] + ]; +*/ + + $ret['url'] = $p['xchan_url']; $ret['publicKey'] = [ - 'id' => $p['xchan_url'], - 'owner' => $p['xchan_url'], - 'publicKeyPem' => $p['xchan_pubkey'] + 'id' => $p['xchan_url'], + 'owner' => $p['xchan_url'], + 'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', + 'publicKeyPem' => $p['xchan_pubkey'] ]; + if ($c) { + $ret['tag'][] = [ + 'type' => 'PropertyValue', + 'name' => 'Protocol', + 'value' => 'zot6' + ]; + + $ret['outbox'] = z_root() . '/outbox/' . $c['channel_address']; + } + $arr = [ - 'xchan' => $p, + 'xchan' => $p, 'encoded' => $ret ]; @@ -1117,8 +1185,8 @@ class Activity { $ret = []; if ($item[$elm]) { - if (! is_array($item[$elm])) { - $item[$elm] = json_decode($item[$elm],true); + if (!is_array($item[$elm])) { + $item[$elm] = json_decode($item[$elm], true); } if ($item[$elm]['type'] === ACTIVITY_OBJ_PHOTO) { $item[$elm]['id'] = $item['mid']; @@ -1148,22 +1216,22 @@ class Activity { } $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/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', - 'Invite' => 'Invite', - 'Delete' => 'Delete', - 'Undo' => 'Undo' + 'http://purl.org/zot/activity/attendyes' => 'Accept', + 'http://purl.org/zot/activity/attendno' => 'Reject', + 'http://purl.org/zot/activity/attendmaybe' => 'TentativeAccept', + 'Invite' => 'Invite', + 'Delete' => 'Delete', + 'Undo' => 'Undo' ]; call_hooks('activity_mapper', $acts); @@ -1196,22 +1264,22 @@ class Activity { 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/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', - 'Invite' => 'Invite', - 'Delete' => 'Delete', - 'Undo' => 'Undo' + 'http://purl.org/zot/activity/attendyes' => 'Accept', + 'http://purl.org/zot/activity/attendno' => 'Reject', + 'http://purl.org/zot/activity/attendmaybe' => 'TentativeAccept', + 'Invite' => 'Invite', + 'Delete' => 'Delete', + 'Undo' => 'Undo' ]; call_hooks('activity_decode_mapper', $acts); @@ -1323,7 +1391,7 @@ class Activity { * */ - if (in_array($act->type, [ 'Follow', 'Invite', 'Join'])) { + if (in_array($act->type, ['Follow', 'Invite', 'Join'])) { $their_follow_id = $act->id; } @@ -1346,8 +1414,8 @@ class Activity { } } - $x = \Zotlabs\Access\PermissionRoles::role_perms('social'); - $their_perms = \Zotlabs\Access\Permissions::FilledPerms($x['perms_connect']); + $x = PermissionRoles::role_perms('personal'); + $their_perms = Permissions::FilledPerms($x['perms_connect']); if ($contact && $contact['abook_id']) { @@ -1421,7 +1489,7 @@ class Activity { } $ret = $r[0]; - $p = \Zotlabs\Access\Permissions::connect_perms($channel['channel_id']); + $p = Permissions::connect_perms($channel['channel_id']); $my_perms = $p['perms']; $automatic = $p['automatic']; @@ -1442,13 +1510,13 @@ class Activity { ] ); - if($my_perms) - foreach($my_perms as $k => $v) - set_abconfig($channel['channel_id'],$ret['xchan_hash'],'my_perms',$k,$v); + if ($my_perms) + foreach ($my_perms as $k => $v) + set_abconfig($channel['channel_id'], $ret['xchan_hash'], 'my_perms', $k, $v); - if($their_perms) - foreach($their_perms as $k => $v) - set_abconfig($channel['channel_id'],$ret['xchan_hash'],'their_perms',$k,$v); + if ($their_perms) + foreach ($their_perms as $k => $v) + set_abconfig($channel['channel_id'], $ret['xchan_hash'], 'their_perms', $k, $v); if ($r) { logger("New ActivityPub follower for {$channel['channel_name']}"); @@ -1463,7 +1531,7 @@ class Activity { 'type' => NOTIFY_INTRO, 'from_xchan' => $ret['xchan_hash'], 'to_xchan' => $channel['channel_hash'], - 'link' => z_root() . '/connedit/' . $new_connection[0]['abook_id'], + 'link' => z_root() . '/connections#' . $new_connection[0]['abook_id'], ] ); @@ -1497,9 +1565,9 @@ class Activity { /* If there is a default group for this channel and permissions are automatic, add this member to it */ if ($channel['channel_default_group'] && $automatic) { - $g = Group::rec_byhash($channel['channel_id'], $channel['channel_default_group']); + $g = AccessList::by_hash($channel['channel_id'], $channel['channel_default_group']); if ($g) - Group::member_add($channel['channel_id'], '', $ret['xchan_hash'], $g['id']); + AccessList::member_add($channel['channel_id'], '', $ret['xchan_hash'], $g['id']); } @@ -1532,34 +1600,128 @@ class Activity { return; } - static function actor_store($url, $person_obj) { + public static function drop($channel, $observer, $act) { + $r = q( + "select * from item where mid = '%s' and uid = %d limit 1", + dbesc((is_array($act->obj)) ? $act->obj['id'] : $act->obj), + intval($channel['channel_id']) + ); + + if (!$r) { + return; + } + + if (in_array($observer, [$r[0]['author_xchan'], $r[0]['owner_xchan']])) { + drop_item($r[0]['id'], false); + } elseif (in_array($act->actor['id'], [$r[0]['author_xchan'], $r[0]['owner_xchan']])) { + drop_item($r[0]['id'], false); + } + } + - if (!is_array($person_obj)) + static function actor_store($url, $person_obj, $force = false) { + + if (!is_array($person_obj)) { return; + } - $inbox = $person_obj['inbox']; + /* 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; - // invalid identity + $hublocs = self::get_actor_hublocs($url); + $has_zot_hubloc = false; + + if ($hublocs) { + foreach ($hublocs as $hub) { + if ($hub['hubloc_network'] === 'activitypub') { + $ap_hubloc = $hub; + } + if ($hub['hubloc_network'] === 'zot6') { + $has_zot_hubloc = true; + 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'] ?? null; + + // invalid AP identity if (!$inbox || strpos($inbox, z_root()) !== false) { return; } + // store the actor record in XConfig + + // we already store this in Activity::fetch() + // XConfig::Set($url, 'system', 'actor_record', $person_obj); + $name = $person_obj['name']; - if (!$name) + if (!$name) { $name = $person_obj['preferredUsername']; - if (!$name) + } + if (!$name) { $name = t('Unknown'); + } + + $webfinger_addr = ''; + $m = parse_url($url); + if ($m) { + $hostname = $m['host']; + $baseurl = $m['scheme'] . '://' . $m['host'] . (($m['port']) ? ':' . $m['port'] : ''); + $site_url = $m['scheme'] . '://' . $m['host']; + } + + if (!empty($person_obj['preferredUsername']) && isset($parsed_url['host'])) { + $webfinger_addr = escape_tags($person_obj['preferredUsername']) . '@' . $hostname; + } + + $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; @@ -1576,7 +1738,7 @@ class Activity { if ($links) { foreach ($links as $link) { - if (array_key_exists('mediaType', $link) && $link['mediaType'] === 'text/html') { + if (is_array($link) && array_key_exists('mediaType', $link) && $link['mediaType'] === 'text/html') { $profile = $link['href']; } } @@ -1592,20 +1754,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']; @@ -1615,66 +1763,68 @@ class Activity { } } - $r = q("select * from xchan where xchan_hash = '%s' limit 1", + $r = q("select * from xchan join hubloc on xchan_hash = hubloc_hash where xchan_hash = '%s'", dbesc($url) ); - if (!$r) { - // create a new record - - xchan_store_lowlevel( - [ - 'xchan_hash' => $url, - 'xchan_guid' => $url, - 'xchan_pubkey' => $pubkey, - 'xchan_addr' => '', - 'xchan_url' => $profile, - 'xchan_name' => $name, - 'xchan_name_date' => datetime_convert(), - 'xchan_network' => 'activitypub' - ] - ); - } - else { + if ($r) { // 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 - 1 week'); - if ($r[0]['xchan_name_date'] > $d) + $d = datetime_convert('UTC', 'UTC', 'now - 3 days'); + if ($r[0]['hubloc_updated'] > $d && !$force) { return; + } - // update existing record - q("update xchan set xchan_name = '%s', xchan_pubkey = '%s', xchan_network = '%s', xchan_name_date = '%s' where xchan_hash = '%s'", - dbesc($name), - dbesc($pubkey), - dbesc('activitypub'), + q("UPDATE site SET site_update = '%s', site_dead = 0 WHERE site_url = '%s'", dbesc(datetime_convert()), + dbesc($site_url) + ); + + // update existing xchan record + q("update xchan set xchan_name = '%s', xchan_guid = '%s', xchan_pubkey = '%s', xchan_addr = '%s', xchan_network = 'activitypub', xchan_name_date = '%s' where xchan_hash = '%s'", + dbesc(escape_tags($name)), + dbesc($url), + dbesc(escape_tags($pubkey)), + dbesc(escape_tags($webfinger_addr)), + dbescdate(datetime_convert()), dbesc($url) ); - } - if ($collections) { - set_xconfig($url, 'activitypub', 'collections', $collections); + // update existing hubloc record + q("update hubloc set hubloc_guid = '%s', hubloc_addr = '%s', hubloc_network = 'activitypub', hubloc_url = '%s', hubloc_host = '%s', hubloc_callback = '%s', hubloc_updated = '%s', hubloc_id_url = '%s' where hubloc_hash = '%s'", + dbesc($url), + dbesc(escape_tags($webfinger_addr)), + dbesc($baseurl), + dbesc($hostname), + dbesc($inbox), + dbescdate(datetime_convert()), + dbesc($profile), + dbesc($url) + ); } + else { + // create a new record - $r = q("select * from hubloc where hubloc_hash = '%s' limit 1", - dbesc($url) - ); - - $m = parse_url($url); - if ($m) { - $hostname = $m['host']; - $site_url = $m['scheme'] . '://' . $m['host'] . (($m['port']) ? ':' . $m['port'] : ''); - } + xchan_store_lowlevel( + [ + 'xchan_hash' => $url, + 'xchan_guid' => $url, + 'xchan_pubkey' => escape_tags($pubkey), + 'xchan_addr' => $webfinger_addr, + 'xchan_url' => escape_tags($profile), + 'xchan_name' => escape_tags($name), + 'xchan_name_date' => datetime_convert(), + 'xchan_network' => 'activitypub' + ] + ); - if (!$r) { hubloc_store_lowlevel( [ 'hubloc_guid' => $url, 'hubloc_hash' => $url, - 'hubloc_addr' => '', + 'hubloc_addr' => $webfinger_addr, 'hubloc_network' => 'activitypub', - 'hubloc_url' => $site_url, + 'hubloc_url' => $baseurl, 'hubloc_host' => $hostname, 'hubloc_callback' => $inbox, 'hubloc_updated' => datetime_convert(), @@ -1684,15 +1834,18 @@ class Activity { ); } - q("UPDATE site SET site_update = '%s', site_dead = 0 WHERE site_url = '%s' AND site_update < %s - INTERVAL %s", - dbesc(datetime_convert()), - dbesc($site_url), - db_utcnow(), - db_quoteinterval('1 DAY') - ); + // We store all ActivityPub actors we can resolve. Some of them may be able to communicate over Zot6. Find them. + // Adding zot discovery urls to the actor record will cause federation to fail with the 20-30 projects which don't accept arrays in the url field. - if (!$icon) - $icon = z_root() . '/' . get_default_profile_photo(300); + $actor_protocols = self::get_actor_protocols($person_obj); + if (!$has_zot_hubloc && in_array('zot6', $actor_protocols)) { + $zx = q("select * from hubloc where hubloc_id_url = '%s' and hubloc_network = 'zot6'", + dbesc($url) + ); + if (!$zx) { + Master::Summon(['Gprobe', bin2hex($url)]); + } + } $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'", @@ -1741,15 +1894,10 @@ class Activity { static function create_note($channel, $observer_hash, $act) { - $s = []; - - // Mastodon only allows visibility in public timelines if the public inbox is listed in the 'to' field. - // They are hidden in the public timeline if the public inbox is listed in the 'cc' field. - // This is not part of the activitypub protocol - we might change this to show all public posts in pubstream at some point. - $pubstream = ((is_array($act->obj) && array_key_exists('to', $act->obj) && in_array(ACTIVITY_PUBLIC_INBOX, $act->obj['to'])) ? true : false); + $s = []; $is_sys_channel = is_sys_channel($channel['channel_id']); + $parent = ((array_key_exists('inReplyTo', $act->obj)) ? urldecode($act->obj['inReplyTo']) : ''); - $parent = ((array_key_exists('inReplyTo', $act->obj)) ? urldecode($act->obj['inReplyTo']) : ''); if ($parent) { $r = q("select * from item where uid = %d and ( mid = '%s' or mid = '%s' ) limit 1", @@ -1764,7 +1912,7 @@ class Activity { } if ($r[0]['owner_xchan'] === $channel['channel_hash']) { - if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') && !($is_sys_channel && $pubstream)) { + if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') && !$is_sys_channel) { logger('no comment permission.'); return; } @@ -1776,17 +1924,27 @@ class Activity { } else { - if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') && !($is_sys_channel && $pubstream)) { - logger('no permission'); + if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') && !$is_sys_channel) { + logger('no send_stream permission'); return; } $s['owner_xchan'] = $s['author_xchan'] = $observer_hash; } - $abook = q("select * from abook where abook_xchan = '%s' and abook_channel = %d limit 1", - dbesc($observer_hash), - intval($channel['channel_id']) - ); + 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; + } + } $content = self::get_content($act->obj); @@ -1865,15 +2023,23 @@ class Activity { } if ($channel['channel_system']) { - if (!MessageFilter::evaluate($s, get_config('system', 'pubstream_incl'), get_config('system', 'pubstream_excl'))) { + $incl = get_config('system','pubstream_incl'); + $excl = get_config('system','pubstream_excl'); + + if(($incl || $excl) && !MessageFilter::evaluate($s, $incl, $excl)) { logger('post is filtered'); return; } } + $abook = q("select * from abook where (abook_xchan = '%s' OR abook_xchan = '%s') and abook_channel = %d ", + dbesc($s['author_xchan']), + dbesc($s['owner_xchan']), + intval($channel['channel_id']) + ); if ($abook) { - if (!post_is_importable($s, $abook[0])) { + if (!post_is_importable($channel['channel_id'], $s, $abook)) { logger('post is filtered'); return; } @@ -1931,14 +2097,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); @@ -1995,6 +2153,7 @@ class Activity { } static function update_poll($item, $post) { + $multi = false; $mid = $post['mid']; $content = $post['title']; @@ -2079,7 +2238,8 @@ class Activity { dbesc(datetime_convert()), intval($item['id']) ); - Master::Summon(['Notifier', 'wall-new', $item['id']]); + + Master::Summon(['Notifier', 'wall-new', $item['id'], $post['mid'] /* trick queueworker de-duplication */ ]); return true; } @@ -2088,38 +2248,64 @@ class Activity { static function decode_note($act) { + $response_activity = false; + + $s = []; + + // These activities should have been handled separately in the Inbox module and should not be turned into posts + + if ( + in_array($act->type, ['Follow', 'Accept', 'Reject', 'Create', 'Update']) && + is_array($act->obj) && + array_key_exists('type', $act->obj) && + ($act->obj['type'] === 'Follow' || ActivityStreams::is_an_actor($act->obj['type'])) + ) { + return false; + } + // Within our family of projects, Follow/Unfollow of a thread is an internal activity which should not be transmitted, // hence if we receive it - ignore or reject it. // Unfollow is not defined by ActivityStreams, which prefers Undo->Follow. // This may have to be revisited if AP projects start using Follow for objects other than actors. - if (in_array($act->type, [ 'Follow', 'Unfollow' ])) { + if (in_array($act->type, ['Follow', 'Unfollow'])) { return false; } - $response_activity = false; + if (!isset($act->actor['id'])) { + logger('No actor!'); + return false; + } - $s = []; + // ensure we store the original actor + self::actor_store($act->actor['id'], $act->actor); + + $s['owner_xchan'] = $act->actor['id']; + $s['author_xchan'] = $act->actor['id']; if (is_array($act->obj)) { $content = self::get_content($act->obj); } - $s['owner_xchan'] = $act->actor['id']; - $s['author_xchan'] = $act->actor['id']; + $s['mid'] = ((is_array($act->obj) && isset($act->obj['id'])) ? $act->obj['id'] : $act->obj); - // ensure we store the original actor - self::actor_store($act->actor['id'], $act->actor); + if (!$s['mid']) { + return false; + } + + // Friendica sends the diaspora guid in a nonstandard field via AP + // If no uuid is provided we will create an uuid v5 from the mid + $s['uuid'] = ((is_array($act->obj) && isset($act->obj['diaspora:guid'])) ? $act->obj['diaspora:guid'] : uuid_from_url($s['mid'])); - $s['mid'] = $act->obj['id']; - $s['uuid'] = $act->obj['diaspora:guid']; $s['parent_mid'] = $act->parent_id; if (array_key_exists('published', $act->data)) { - $s['created'] = datetime_convert('UTC', 'UTC', $act->data['published']); + $s['created'] = datetime_convert('UTC', 'UTC', $act->data['published']); + $s['commented'] = $s['created']; } elseif (array_key_exists('published', $act->obj)) { - $s['created'] = datetime_convert('UTC', 'UTC', $act->obj['published']); + $s['created'] = datetime_convert('UTC', 'UTC', $act->obj['published']); + $s['commented'] = $s['created']; } if (array_key_exists('updated', $act->data)) { $s['edited'] = datetime_convert('UTC', 'UTC', $act->data['updated']); @@ -2134,13 +2320,18 @@ class Activity { $s['expires'] = datetime_convert('UTC', 'UTC', $act->obj['expires']); } + if ($act->type === 'Invite' && is_array($act->obj) && array_key_exists('type', $act->obj) && $act->obj['type'] === 'Event') { + $s['mid'] = $s['parent_mid'] = $act->id; + } + if (ActivityStreams::is_response_activity($act->type)) { $response_activity = true; $s['mid'] = $act->id; - // $s['parent_mid'] = $act->obj['id']; - $s['uuid'] = $act->data['diaspora:guid']; + $s['uuid'] = ((is_array($act->data) && isset($act->data['diaspora:guid'])) ? $act->data['diaspora:guid'] : uuid_from_url($s['mid'])); + + $s['parent_mid'] = ((is_array($act->obj) && isset($act->obj['id'])) ? $act->obj['id'] : $act->obj); // over-ride the object timestamp with the activity @@ -2153,8 +2344,8 @@ class Activity { } $obj_actor = ((isset($act->obj['actor'])) ? $act->obj['actor'] : $act->get_actor('attributedTo', $act->obj)); - // ensure we store the original actor + // ensure we store the original actor self::actor_store($obj_actor['id'], $obj_actor); $mention = self::get_actor_bbmention($obj_actor['id']); @@ -2183,23 +2374,61 @@ class Activity { } if ($act->type === 'Announce') { - $content['content'] = sprintf(t('🔁 Repeated %1$s\'s %2$s'), $mention, $act->obj['type']); + $s['author_xchan'] = $obj_actor['id']; + $s['mid'] = $act->obj['id']; + $s['parent_mid'] = $act->obj['id']; } if ($act->type === 'emojiReaction') { $content['content'] = (($act->tgt && $act->tgt['type'] === 'Image') ? '[img=32x32]' . $act->tgt['url'] . '[/img]' : '&#x' . $act->tgt['name'] . ';'); } } - if (! array_key_exists('created', $s)) + $s['item_thread_top'] = 0; + $s['comment_policy'] = 'authenticated'; + + if ($s['mid'] === $s['parent_mid']) { + $s['item_thread_top'] = 1; + + // it is a parent node - decode the comment policy info if present + if (isset($act->obj['commentPolicy'])) { + $until = strpos($act->obj['commentPolicy'], 'until='); + 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; + } + } + + $remainder = substr($act->obj['commentPolicy'], 0, (($until) ? $until : strlen($act->obj['commentPolicy']))); + if ($remainder) { + $s['comment_policy'] = $remainder; + } + if (!(isset($item['comment_policy']) && strlen($item['comment_policy']))) { + $s['comment_policy'] = 'contacts'; + } + } + } + + if (!array_key_exists('created', $s)) $s['created'] = datetime_convert(); - if (! array_key_exists('edited', $s)) + if (!array_key_exists('edited', $s)) $s['edited'] = $s['created']; $s['title'] = (($response_activity) ? EMPTY_STR : self::bb_content($content, 'name')); $s['summary'] = self::bb_content($content, 'summary'); $s['body'] = ((self::bb_content($content, 'bbcode') && (!$response_activity)) ? self::bb_content($content, 'bbcode') : self::bb_content($content, 'content')); + if (isset($act->obj['quoteUrl'])) { + $quote_bbcode = self::get_quote_bbcode($act->obj['quoteUrl']); + + if ($s['body']) { + $s['body'] .= "\r\n\r\n"; + } + + $s['body'] .= $quote_bbcode; + } + $s['verb'] = self::activity_decode_mapper($act->type); // Mastodon does not provide update timestamps when updating poll tallies which means race conditions may occur here. @@ -2216,6 +2445,12 @@ class Activity { $s['obj_type'] = ACTIVITY_OBJ_COMMENT; } + $s['obj'] = $act->obj; + if (is_array($s['obj']) && array_path_exists('actor/id', $s['obj'])) { + $s['obj']['actor'] = $s['obj']['actor']['id']; + } + +/* $eventptr = null; if ($act->obj['type'] === 'Invite' && array_path_exists('object/type', $act->obj) && $act->obj['object']['type'] === 'Event') { @@ -2236,19 +2471,19 @@ class Activity { $s['obj']['asld'] = $eventptr; $s['obj']['type'] = ACTIVITY_OBJ_EVENT; $s['obj']['id'] = $eventptr['id']; - $s['obj']['title'] = $eventptr['name']; + $s['obj']['title'] = html2plain($eventptr['name']); if (strpos($act->obj['startTime'], 'Z')) $s['obj']['adjust'] = true; else - $s['obj']['adjust'] = false; + $s['obj']['adjust'] = true; $s['obj']['dtstart'] = datetime_convert('UTC', 'UTC', $eventptr['startTime']); if ($act->obj['endTime']) $s['obj']['dtend'] = datetime_convert('UTC', 'UTC', $eventptr['endTime']); else $s['obj']['nofinish'] = true; - $s['obj']['description'] = $eventptr['content']; + $s['obj']['description'] = html2bbcode($eventptr['content']); if (array_path_exists('location/content', $eventptr)) $s['obj']['location'] = $eventptr['location']['content']; @@ -2257,6 +2492,7 @@ class Activity { else { $s['obj'] = $act->obj; } +*/ $generator = $act->get_property_obj('generator'); if ((!$generator) && (!$response_activity)) { @@ -2268,7 +2504,6 @@ class Activity { $s['app'] = escape_tags($generator['name']); } - if (!$response_activity) { $a = self::decode_taxonomy($act->obj); if ($a) { @@ -2297,7 +2532,7 @@ class Activity { if (array_key_exists('type', $act->obj)) { if ($act->obj['type'] === 'Note' && $s['attach']) { - $s['body'] .= self::bb_attach($s['attach'], $s['body']); + $s['body'] = self::bb_attach($s['attach'], $s['body']) . $s['body']; } if ($act->obj['type'] === 'Question' && in_array($act->type, ['Create', 'Update'])) { @@ -2325,31 +2560,57 @@ class Activity { 'video/webm' ]; - $mps = []; + $mps = []; + $poster = null; + $ptr = null; + + // try to find a poster to display on the video element + + if (array_key_exists('icon',$act->obj)) { + if (is_array($act->obj['icon'])) { + if (array_key_exists(0,$act->obj['icon'])) { + $ptr = $act->obj['icon']; + } + else { + $ptr = [ $act->obj['icon'] ]; + } + } + if ($ptr) { + foreach ($ptr as $foo) { + if (is_array($foo) && array_key_exists('type',$foo) && $foo['type'] === 'Image' && is_string($foo['url'])) { + $poster = $foo['url']; + } + } + } + } + + $tag = (($poster) ? '[video poster="' . $poster . '"]' : '[video]' ); $ptr = null; - if (array_key_exists('url', $act->obj)) { + if (array_key_exists('url',$act->obj)) { if (is_array($act->obj['url'])) { - if (array_key_exists(0, $act->obj['url'])) { + if (array_key_exists(0,$act->obj['url'])) { $ptr = $act->obj['url']; } else { - $ptr = [$act->obj['url']]; + $ptr = [ $act->obj['url'] ]; } - foreach ($ptr as $vurl) { - // peertube uses the non-standard element name 'mimeType' here - if (array_key_exists('mimeType', $vurl)) { - if (in_array($vurl['mimeType'], $vtypes)) { - if (!array_key_exists('width', $vurl)) { - $vurl['width'] = 0; - } - $mps[] = $vurl; + // handle peertube's weird url link tree if we find it here + // 0 => html link, 1 => application/x-mpegURL with 'tag' set to an array of actual media links + foreach ($ptr as $idex) { + if (is_array($idex) && array_key_exists('mediaType',$idex)) { + if ($idex['mediaType'] === 'application/x-mpegURL' && isset($idex['tag']) && is_array($idex['tag'])) { + $ptr = $idex['tag']; + break; } } - elseif (array_key_exists('mediaType', $vurl)) { + } + + foreach ($ptr as $vurl) { + if (array_key_exists('mediaType',$vurl)) { if (in_array($vurl['mediaType'], $vtypes)) { - if (!array_key_exists('width', $vurl)) { - $vurl['width'] = 0; + if (! array_key_exists('height',$vurl)) { + $vurl['height'] = 0; } $mps[] = $vurl; } @@ -2357,17 +2618,18 @@ class Activity { } } if ($mps) { - usort($mps, [__CLASS__, 'vid_sort']); + usort($mps,[ '\Zotlabs\Lib\Activity', 'vid_sort' ]); foreach ($mps as $m) { - if (intval($m['width']) < 500 && self::media_not_in_body($m['href'], $s['body'])) { - $s['body'] .= "\n\n" . '[video]' . $m['href'] . '[/video]'; + if (intval($m['height']) < 500 && Activity::media_not_in_body($m['href'],$s['body'])) { + $s['body'] = $tag . $m['href'] . '[/video]' . "\n\n" . $s['body']; break; } } } - elseif (is_string($act->obj['url']) && self::media_not_in_body($act->obj['url'], $s['body'])) { - $s['body'] .= "\n\n" . '[video]' . $act->obj['url'] . '[/video]'; + elseif (is_string($act->obj['url']) && Activity::media_not_in_body($act->obj['url'],$s['body'])) { + $s['body'] = $tag . $act->obj['url'] . '[/video]' . "\n\n" . $s['body']; } + } } @@ -2391,19 +2653,19 @@ class Activity { } foreach ($ptr as $vurl) { if (in_array($vurl['mediaType'], $atypes) && self::media_not_in_body($vurl['href'], $s['body'])) { - $s['body'] .= "\n\n" . '[audio]' . $vurl['href'] . '[/audio]'; + $s['body'] = '[audio]' . $vurl['href'] . '[/audio]' . "\n\n" . $s['body']; break; } } } elseif (is_string($act->obj['url']) && self::media_not_in_body($act->obj['url'], $s['body'])) { - $s['body'] .= "\n\n" . '[audio]' . $act->obj['url'] . '[/audio]'; + $s['body'] = '[audio]' . $act->obj['url'] . '[/audio]' . "\n\n" . $s['body']; } } } - if ($act->obj['type'] === 'Image') { + if ($act->obj['type'] === 'Image' && strpos($s['body'], 'zrl=') === false) { $ptr = null; @@ -2472,7 +2734,6 @@ class Activity { } } - if (in_array($act->obj['type'], ['Note', 'Article', 'Page'])) { $ptr = null; @@ -2515,13 +2776,78 @@ class Activity { } } - set_iconfig($s, 'activitypub', 'recips', $act->raw_recips); + $ap_rawmsg = ''; + $diaspora_rawmsg = ''; + $raw_arr = []; - $parent = (($s['parent_mid'] && $s['parent_mid'] === $s['mid']) ? true : false); - if ($parent) { + $raw_arr = json_decode($act->raw, true); + + // This is a zot6 packet and the raw activitypub or diaspora message json + // is possibly available in the attachement. + if (array_key_exists('signed', $raw_arr) && is_array($act->data['attachment'])) { + foreach($act->data['attachment'] as $a) { + if ( + isset($a['type']) && $a['type'] === 'PropertyValue' && + isset($a['name']) && $a['name'] === 'zot.activitypub.rawmsg' && + isset($a['value']) + ) { + $ap_rawmsg = $a['value']; + } + if ( + isset($a['type']) && $a['type'] === 'PropertyValue' && + isset($a['name']) && $a['name'] === 'zot.diaspora.fields' && + isset($a['value']) + ) { + $diaspora_rawmsg = $a['value']; + } + } + } + + // old style: can be removed after most hubs are on 7.0.2 + elseif (array_key_exists('signed', $raw_arr) && is_array($act->obj) && is_array($act->obj['attachment'])) { + foreach($act->obj['attachment'] as $a) { + if ( + isset($a['type']) && $a['type'] === 'PropertyValue' && + isset($a['name']) && $a['name'] === 'zot.activitypub.rawmsg' && + isset($a['value']) + ) { + $ap_rawmsg = $a['value']; + } + + if ( + isset($a['type']) && $a['type'] === 'PropertyValue' && + isset($a['name']) && $a['name'] === 'zot.diaspora.fields' && + isset($a['value']) + ) { + $diaspora_rawmsg = $a['value']; + } + } + } + + // catch the likes + if (!$ap_rawmsg && $response_activity) { + $ap_rawmsg = json_encode($act->data, JSON_UNESCAPED_SLASHES); + } + // end old style + + if (!$ap_rawmsg && array_key_exists('signed', $raw_arr)) { + // zap + $ap_rawmsg = json_encode($act->data, JSON_UNESCAPED_SLASHES); + } + + if ($ap_rawmsg) { + set_iconfig($s, 'activitypub', 'rawmsg', $ap_rawmsg, 1); + } + elseif (!array_key_exists('signed', $raw_arr)) { set_iconfig($s, 'activitypub', 'rawmsg', $act->raw, 1); } + if ($diaspora_rawmsg) { + set_iconfig($s, 'diaspora', 'fields', $diaspora_rawmsg, 1); + } + + set_iconfig($s, 'activitypub', 'recips', $act->raw_recips); + $hookinfo = [ 'act' => $act, 's' => $s @@ -2545,18 +2871,6 @@ class Activity { return; }*/ - // Mastodon only allows visibility in public timelines if the public inbox is listed in the 'to' field. - // They are hidden in the public timeline if the public inbox is listed in the 'cc' field. - // This is not part of the activitypub protocol - we might change this to show all public posts in pubstream at some point. - - $pubstream = ((is_array($act->obj) && array_key_exists('to', $act->obj) && in_array(ACTIVITY_PUBLIC_INBOX, $act->obj['to'])) ? true : false); - - // TODO: this his handled in pubcrawl atm. - // very unpleasant and imperfect way of determining a Mastodon DM - /*if ($act->raw_recips && array_key_exists('to',$act->raw_recips) && is_array($act->raw_recips['to']) && count($act->raw_recips['to']) === 1 && $act->raw_recips['to'][0] === channel_url($channel) && ! $act->raw_recips['cc']) { - $item['item_private'] = 2; - }*/ - if ($item['parent_mid'] && $item['parent_mid'] !== $item['mid']) { $is_child_node = true; } @@ -2576,6 +2890,17 @@ class Activity { // set the owner to the owner of the parent $item['owner_xchan'] = $p[0]['owner_xchan']; + // quietly reject group comment boosts by group owner + // (usually only sent via ActivityPub so groups will work on microblog platforms) + // This catches those activities if they slipped in via a conversation fetch + + if ($p[0]['parent_mid'] !== $item['parent_mid']) { + if ($item['verb'] === 'Announce' && $item['author_xchan'] === $item['owner_xchan']) { + logger('group boost activity by group owner rejected'); + return; + } + } + // check permissions against the author, not the sender $allowed = perm_is_allowed($channel['channel_id'], $item['author_xchan'], 'post_comments'); if ((!$allowed)/* && $permit_mentions*/) { @@ -2611,7 +2936,7 @@ class Activity { $allowed = true; // reject public stream comments that weren't sent by the conversation owner - if ($is_sys_channel && $pubstream && $item['owner_xchan'] !== $observer_hash && !$fetch_parents) { + if ($is_sys_channel && $item['owner_xchan'] !== $observer_hash && !$fetch_parents) { $allowed = false; } } @@ -2626,7 +2951,7 @@ class Activity { // The $item['item_fetched'] flag is set in fetch_and_store_parents(). // In this case we should check against author permissions because sender is not owner. - if (perm_is_allowed($channel['channel_id'], (($item['item_fetched']) ? $item['author_xchan'] : $observer_hash), 'send_stream') || ($is_sys_channel && $pubstream)) { + if (perm_is_allowed($channel['channel_id'], (($item['item_fetched']) ? $item['author_xchan'] : $observer_hash), 'send_stream') || $is_sys_channel) { $allowed = true; } // TODO: not implemented @@ -2641,6 +2966,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) { @@ -2705,19 +3035,23 @@ class Activity { return; if ($channel['channel_system']) { - if (!MessageFilter::evaluate($item, get_config('system', 'pubstream_incl'), get_config('system', 'pubstream_excl'))) { + $incl = get_config('system','pubstream_incl'); + $excl = get_config('system','pubstream_excl'); + + if(($incl || $excl) && !MessageFilter::evaluate($item, $incl, $excl)) { logger('post is filtered'); return; } } - $abook = q("select * from abook where abook_xchan = '%s' and abook_channel = %d limit 1", - dbesc($observer_hash), + $abook = q("select * from abook where ( abook_xchan = '%s' OR abook_xchan = '%s') and abook_channel = %d ", + dbesc($item['author_xchan']), + dbesc($item['owner_xchan']), intval($channel['channel_id']) ); if ($abook) { - if (!post_is_importable($item, $abook[0])) { + if (!post_is_importable($channel['channel_id'], $item, $abook)) { logger('post is filtered'); return; } @@ -2764,7 +3098,7 @@ class Activity { $fetch = false; // TODO: debug // if (perm_is_allowed($channel['channel_id'],$observer_hash,'send_stream') && (PConfig::Get($channel['channel_id'],'system','hyperdrive',true) || $act->type === 'Announce')) { - if (perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') || ($is_sys_channel && $pubstream)) { + if (perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') || $is_sys_channel) { $fetch = (($fetch_parents) ? self::fetch_and_store_parents($channel, $observer_hash, $item, $force) : false); } if ($fetch) { @@ -2787,6 +3121,19 @@ class Activity { $item['thr_parent'] = $parent[0]['parent_mid']; } $item['parent_mid'] = $parent[0]['parent_mid']; + //$item['item_private'] = $parent[0]['item_private']; + + } + + // An ugly and imperfect way to recognise a mastodon direct message + if ( + $item['item_private'] === 1 && + !isset($act->raw_recips['cc']) && + is_array($act->raw_recips['to']) && + in_array(channel_url($channel), $act->raw_recips['to']) && + !in_array($act->actor['followers'], $act->raw_recips['to']) + ) { + $item['item_private'] = 2; } // TODO: not implemented @@ -2797,6 +3144,12 @@ class Activity { intval($item['uid']) ); if ($r) { + + // If we already have the item, dismiss its announce + if ($act->type === 'Announce') { + return; + } + if ($item['edited'] > $r[0]['edited']) { $item['id'] = $r[0]['id']; $x = item_store_update($item); @@ -2813,12 +3166,12 @@ class Activity { logger('topfetch', LOGGER_DEBUG); // if the thread owner is a connnection, we will already receive any additional comments to their posts // but if they are not we can try to fetch others in the background - $x = q("SELECT abook.*, xchan.* FROM abook left join xchan on abook_xchan = xchan_hash + $connected = q("SELECT abook.*, xchan.* FROM abook left join xchan on abook_xchan = xchan_hash WHERE abook_channel = %d and abook_xchan = '%s' LIMIT 1", intval($channel['channel_id']), dbesc($parent[0]['owner_xchan']) ); - if (!$x) { + if (!$connected) { // determine if the top-level post provides a replies collection if ($parent[0]['obj']) { $parent[0]['obj'] = json_decode($parent[0]['obj'], true); @@ -3070,18 +3423,13 @@ class Activity { } +/* this is deprecated and not used anymore static function announce_note($channel, $observer_hash, $act) { - $s = []; - + $s = []; $is_sys_channel = is_sys_channel($channel['channel_id']); - // Mastodon only allows visibility in public timelines if the public inbox is listed in the 'to' field. - // They are hidden in the public timeline if the public inbox is listed in the 'cc' field. - // This is not part of the activitypub protocol - we might change this to show all public posts in pubstream at some point. - $pubstream = ((is_array($act->obj) && array_key_exists('to', $act->obj) && in_array(ACTIVITY_PUBLIC_INBOX, $act->obj['to'])) ? true : false); - - if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') && !($is_sys_channel && $pubstream)) { + if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') && !$is_sys_channel) { logger('no permission'); return; } @@ -3206,6 +3554,7 @@ class Activity { } } +*/ static function like_note($channel, $observer_hash, $act) { @@ -3338,7 +3687,7 @@ class Activity { $ret = false; foreach ($attach as $a) { - if (array_key_exists('type',$a) && stripos($a['type'], 'image') !== false) { + if (array_key_exists('type', $a) && stripos($a['type'], 'image') !== false) { if (self::media_not_in_body($a['href'], $body)) { $ret .= "\n\n" . '[img]' . $a['href'] . '[/img]'; } @@ -3426,7 +3775,49 @@ class Activity { $event['nofinish'] = true; } } +/* + $eventptr = null; + + if ($act->obj['type'] === 'Invite' && array_path_exists('object/type', $act->obj) && $act->obj['object']['type'] === 'Event') { + $eventptr = $act->obj['object']; + $s['mid'] = $s['parent_mid'] = $act->obj['id']; + } + + if ($act->obj['type'] === 'Event') { + if ($act->type === 'Invite') { + $s['mid'] = $s['parent_mid'] = $act->id; + } + $eventptr = $act->obj; + } + + if ($eventptr) { + $s['obj'] = []; + $s['obj']['asld'] = $eventptr; + $s['obj']['type'] = ACTIVITY_OBJ_EVENT; + $s['obj']['id'] = $eventptr['id']; + $s['obj']['title'] = html2plain($eventptr['name']); + + if (strpos($act->obj['startTime'], 'Z')) + $s['obj']['adjust'] = true; + else + $s['obj']['adjust'] = true; + + $s['obj']['dtstart'] = datetime_convert('UTC', 'UTC', $eventptr['startTime']); + if ($act->obj['endTime']) + $s['obj']['dtend'] = datetime_convert('UTC', 'UTC', $eventptr['endTime']); + else + $s['obj']['nofinish'] = true; + $s['obj']['description'] = html2bbcode($eventptr['content']); + + if (array_path_exists('location/content', $eventptr)) + $s['obj']['location'] = $eventptr['location']['content']; + + } + else { + $s['obj'] = $act->obj; + } +*/ foreach (['name', 'summary', 'content'] as $a) { if (($x = self::get_textfield($act, $a)) !== false) { $content[$a] = $x; @@ -3496,7 +3887,7 @@ class Activity { static function find_best_identity($xchan) { if (filter_var($xchan, FILTER_VALIDATE_URL)) { - $r = q("select hubloc_hash, hubloc_network from hubloc where hubloc_id_url = '%s' and hubloc_network in ('zot6', 'zot') and hubloc_deleted = 0", + $r = q("SELECT hubloc_hash, hubloc_network FROM hubloc WHERE hubloc_id_url = '%s' AND hubloc_network IN ('zot6', 'activitypub') AND hubloc_deleted = 0", dbesc($xchan) ); if ($r) { @@ -3510,4 +3901,116 @@ class Activity { } + static function get_cached_actor($id) { + + // remove any fragments like #main-key since these won't be present in our cached data + $cache_url = ((strpos($id, '#')) ? substr($id, 0, strpos($id, '#')) : $id); + $actor = XConfig::Get($cache_url, 'system', 'actor_record'); + + if ($actor) { + return $actor; + } + + // try other get_cached_actor providers (e.g. diaspora) + $hookdata = [ + 'id' => $id, + 'actor' => null + ]; + + call_hooks('get_cached_actor_provider', $hookdata); + + return $hookdata['actor']; + } + + static function get_actor_hublocs($url, $options = 'all') { + + 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; + } + + + static function get_actor_protocols($actor) { + $ret = []; + + if (!array_key_exists('tag', $actor) || empty($actor['tag']) || !is_array($actor['tag'])) { + return $ret; + } + + foreach ($actor['tag'] as $t) { + if ((isset($t['type']) && $t['type'] === 'PropertyValue') && + (isset($t['name']) && $t['name'] === 'Protocol') && + (isset($t['value']) && in_array($t['value'], ['zot6', 'activitypub', 'diaspora'])) + ) { + $ret[] = $t['value']; + } + } + + return $ret; + } + + static function get_quote_bbcode($url) { + + $ret = ''; + + $a = self::fetch($url); + if ($a) { + $act = new ActivityStreams($a); + + if ($act->is_valid()) { + $content = self::get_content($act->obj); + + $ret .= "[share author='" . urlencode($act->actor['name']) . + "' profile='" . $act->actor['id'] . + "' avatar='" . $act->actor['icon']['url'] . + "' link='" . $act->obj['id'] . + "' auth='" . ((is_matrix_url($act->actor['id'])) ? 'true' : 'false') . + "' posted='" . $act->obj['published'] . + "' message_id='" . $act->obj['id'] . + "']"; + $ret .= self::bb_content($content, 'content'); + $ret .= '[/share]'; + } + } + + return $ret; + } + } diff --git a/Zotlabs/Lib/ActivityStreams.php b/Zotlabs/Lib/ActivityStreams.php index a5fb4a756..1c278f2ee 100644 --- a/Zotlabs/Lib/ActivityStreams.php +++ b/Zotlabs/Lib/ActivityStreams.php @@ -11,6 +11,7 @@ class ActivityStreams { public $raw = null; public $data = null; + public $meta = null; public $valid = false; public $deleted = false; public $id = ''; @@ -36,10 +37,14 @@ class ActivityStreams { */ function __construct($string) { + if(!$string) + return; + $this->raw = $string; if (is_array($string)) { $this->data = $string; + $this->raw = json_encode($string, JSON_UNESCAPED_SLASHES); } else { $this->data = json_decode($string, true); @@ -56,10 +61,10 @@ class ActivityStreams { if ($ret['signer']) { $saved = json_encode($this->data, JSON_UNESCAPED_SLASHES); $this->data = $tmp; - $this->data['signer'] = $ret['signer']; - $this->data['signed_data'] = $saved; + $this->meta['signer'] = $ret['signer']; + $this->meta['signed_data'] = $saved; if ($ret['hubloc']) { - $this->data['hubloc'] = $ret['hubloc']; + $this->meta['hubloc'] = $ret['hubloc']; } } } @@ -87,7 +92,7 @@ class ActivityStreams { $this->ldsig = $this->get_compound_property('signature'); if ($this->ldsig) { - $this->signer = $this->get_compound_property('creator', $this->ldsig); + $this->signer = $this->get_actor('creator', $this->ldsig); if ($this->signer && is_array($this->signer) && array_key_exists('publicKey', $this->signer) && is_array($this->signer['publicKey']) && $this->signer['publicKey']['publicKeyPem']) { $this->sigok = LDSignatures::verify($this->data, $this->signer['publicKey']['publicKeyPem']); } @@ -285,7 +290,7 @@ class ActivityStreams { if (!$s) { return false; } - return (in_array($s, ['Like', 'Dislike', 'Flag', 'Block', 'Accept', 'Reject', 'TentativeAccept', 'TentativeReject', 'emojiReaction', 'EmojiReaction', 'EmojiReact'])); + return (in_array($s, ['Like', 'Dislike', 'Flag', 'Block', 'Announce', 'Accept', 'Reject', 'TentativeAccept', 'TentativeReject', 'emojiReaction', 'EmojiReaction', 'EmojiReact'])); } /** @@ -300,16 +305,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_network in ('zot6', 'activitypub') and hubloc_id_url = '%s'", - dbesc($x) - ); - if ($r) { - $y = Activity::encode_person($r[0]); - $y['cached'] = true; + $y = Activity::get_cached_actor($x); + if ($y) { return $y; } } @@ -351,10 +348,10 @@ class ActivityStreams { if ($ret['signer']) { $saved = json_encode($x, JSON_UNESCAPED_SLASHES); $x = $tmp; - $x['signer'] = $ret['signer']; - $x['signed_data'] = $saved; + $x['meta']['signer'] = $ret['signer']; + $x['meta']['signed_data'] = $saved; if ($ret['hubloc']) { - $x['hubloc'] = $ret['hubloc']; + $x['meta']['hubloc'] = $ret['hubloc']; } } } @@ -425,15 +422,19 @@ class ActivityStreams { static function get_accept_header_string($channel = null) { + $ret = ''; + $hookdata = []; if ($channel) $hookdata['channel'] = $channel; - $hookdata['data'] = 'application/x-zot-activity+json'; + $hookdata['data'] = ['application/x-zot-activity+json']; call_hooks('get_accept_header_string', $hookdata); - return $hookdata['data']; + $ret = implode(', ', $hookdata['data']); + + return $ret; } diff --git a/Zotlabs/Lib/Apps.php b/Zotlabs/Lib/Apps.php index 5ef4ecc8d..98ebc546a 100644 --- a/Zotlabs/Lib/Apps.php +++ b/Zotlabs/Lib/Apps.php @@ -2,7 +2,7 @@ namespace Zotlabs\Lib; -use Zotlabs\Lib\Libsync; +use App; require_once('include/plugin.php'); require_once('include/channel.php'); @@ -21,9 +21,10 @@ class Apps { * @brief * * @param boolean $translate (optional) default true + * @param boolean $sync (optional) default false used if called from sync_sysapps() * @return array */ - static public function get_system_apps($translate = true) { + static public function get_system_apps($translate = true, $sync = false) { $ret = []; if(is_dir('apps')) @@ -33,7 +34,7 @@ class Apps { if($files) { foreach($files as $f) { - $x = self::parse_app_description($f,$translate); + $x = self::parse_app_description($f, $translate, $sync); if($x) { $ret[] = $x; } @@ -45,7 +46,7 @@ class Apps { $path = explode('/',$f); $plugin = trim($path[1]); if(plugin_is_installed($plugin)) { - $x = self::parse_app_description($f,$translate); + $x = self::parse_app_description($f, $translate, $sync); if($x) { $x['plugin'] = $plugin; $ret[] = $x; @@ -66,17 +67,17 @@ class Apps { static public function get_base_apps() { $x = get_config('system','base_apps',[ 'Connections', + 'Contact Roles', 'Network', - 'Settings', 'Files', - 'Channel Home', - 'View Profile', + 'Channel', 'Photos', 'Calendar', 'Directory', 'Search', 'Help', - 'Profile Photo' + 'HQ', + 'Post' ]); /** @@ -207,9 +208,10 @@ class Apps { * * @param string $f filename * @param boolean $translate (optional) default true + * @param boolean $sync (optional) default false * @return boolean|array */ - static public function parse_app_description($f, $translate = true) { + static public function parse_app_description($f, $translate = true, $sync = false) { $ret = []; $matches = []; @@ -255,7 +257,7 @@ class Apps { if(array_key_exists('categories',$ret)) $ret['categories'] = str_replace(array('\'','"'),array(''','&dquot;'),$ret['categories']); - if(array_key_exists('requires',$ret)) { + if(array_key_exists('requires',$ret) && !$sync) { $requires = explode(',',$ret['requires']); foreach($requires as $require) { $require = trim(strtolower($require)); @@ -307,14 +309,16 @@ class Apps { } } } - if(isset($ret)) { - if($translate) - self::translate_system_apps($ret); - return $ret; + if(empty($ret)) { + return false; } - return false; + if($translate) { + self::translate_system_apps($ret); + } + + return $ret; } @@ -340,7 +344,7 @@ class Apps { 'Files' => t('Files'), 'Webpages' => t('Webpages'), 'Wiki' => t('Wiki'), - 'Channel Home' => t('Channel Home'), + 'Channel' => t('Channel'), 'View Profile' => t('View Profile'), 'Photos' => t('Photos'), 'Calendar' => t('Calendar'), @@ -371,7 +375,7 @@ class Apps { 'OAuth Apps Manager' => t('OAuth Apps Manager'), 'OAuth2 Apps Manager' => t('OAuth2 Apps Manager'), 'PDL Editor' => t('PDL Editor'), - 'Permission Categories' => t('Permission Categories'), + 'Contact Roles' => t('Contact Roles'), 'Public Stream' => t('Public Stream'), 'My Chatrooms' => t('My Chatrooms'), 'Channel Export' => t('Channel Export') @@ -422,7 +426,7 @@ class Apps { self::translate_system_apps($papp); - if(trim($papp['plugin']) && (! plugin_is_installed(trim($papp['plugin'])))) + if(isset($papp['plugin']) && trim($papp['plugin']) && (! plugin_is_installed(trim($papp['plugin'])))) return ''; $papp['papp'] = self::papp_encode($papp); @@ -524,7 +528,7 @@ class Apps { } elseif(remote_channel()) { $observer = \App::get_observer(); - if($observer && in_array($observer['xchan_network'], ['zot6', 'zot'])) { + if($observer && $observer['xchan_network'] === 'zot6') { // some folks might have xchan_url redirected offsite, use the connurl $x = parse_url($observer['xchan_connurl']); if($x) { @@ -536,13 +540,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; } @@ -563,8 +601,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'), @@ -574,6 +610,7 @@ class Apps { )); } + static public function app_install($uid,$app) { if(! is_array($app)) { @@ -588,10 +625,12 @@ class Apps { $app['uid'] = $uid; - if(self::app_installed($uid,$app,true)) + if(self::app_installed($uid,$app,true)) { $x = self::app_update($app); - else + } + else { $x = self::app_store($app); + } if($x['success']) { $r = q("select * from app where app_id = '%s' and app_channel = %d limit 1", @@ -599,13 +638,12 @@ class Apps { intval($uid) ); if($r) { - if(($app['uid']) && (! $r[0]['app_system'])) { + if($app['uid']) { if($app['categories'] && (! $app['term'])) { $r[0]['term'] = q("select * from term where otype = %d and oid = %d", intval(TERM_OBJ_APP), intval($r[0]['id']) ); - Libsync::build_sync_packet($uid,array('app' => $r[0])); } } } @@ -634,6 +672,7 @@ class Apps { } } } + return true; } @@ -645,38 +684,35 @@ class Apps { dbesc($app['guid']), intval($uid) ); - if($x) { - if(! intval($x[0]['app_deleted'])) { - $x[0]['app_deleted'] = 1; - if(self::can_delete($uid,$app)) { - q("delete from app where app_id = '%s' and app_channel = %d", - dbesc($app['guid']), - intval($uid) - ); - q("delete from term where otype = %d and oid = %d", - intval(TERM_OBJ_APP), - intval($x[0]['id']) - ); - /** - * @hooks app_destroy - * Called after app entry got removed from database - * and provide app array from database. - */ - call_hooks('app_destroy', $x[0]); - } - else { - q("update app set app_deleted = 1 where app_id = '%s' and app_channel = %d", - dbesc($app['guid']), - intval($uid) - ); - } - if(! intval($x[0]['app_system'])) { - Libsync::build_sync_packet($uid,array('app' => $x)); - } - } - else { - self::app_undestroy($uid,$app); - } + + if($x && intval($x[0]['app_deleted'])) { + self::app_undestroy($uid, $app); + return; + } + + if(self::can_delete($uid,$app)) { + q("delete from app where app_id = '%s' and app_channel = %d", + dbesc($app['guid']), + intval($uid) + ); + + q("delete from term where otype = %d and oid = %d", + intval(TERM_OBJ_APP), + intval($x[0]['id']) + ); + + /** + * @hooks app_destroy + * Called after app entry got removed from database + * and provide app array from database. + */ + call_hooks('app_destroy', $x[0]); + } + else { + q("update app set app_deleted = 1 where app_id = '%s' and app_channel = %d", + dbesc($app['guid']), + intval($uid) + ); } } } @@ -693,13 +729,11 @@ class Apps { dbesc($app['guid']), intval($uid) ); - if($x) { - if($x[0]['app_system']) { - q("update app set app_deleted = 0 where app_id = '%s' and app_channel = %d", - dbesc($app['guid']), - intval($uid) - ); - } + if($x && intval($x[0]['app_deleted']) && $x[0]['app_system']) { + q("update app set app_deleted = 0 where app_id = '%s' and app_channel = %d", + dbesc($app['guid']), + intval($uid) + ); } } } @@ -1158,9 +1192,9 @@ class Apps { $y = explode(',',$arr['categories']); if($y) { foreach($y as $t) { - $t = trim($t); + $t = escape_tags(trim($t)); if($t) { - store_item_tag($darray['app_channel'],$x[0]['id'],TERM_OBJ_APP,TERM_CATEGORY,escape_tags($t),escape_tags(z_root() . '/apps/?f=&cat=' . escape_tags($t))); + store_item_tag($darray['app_channel'], $x[0]['id'], TERM_OBJ_APP, TERM_CATEGORY, $t, z_root() . '/apps/?f=&cat=' . $t); } } } @@ -1356,4 +1390,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; + } } diff --git a/Zotlabs/Lib/Connect.php b/Zotlabs/Lib/Connect.php index 21bec171b..0b9ff7089 100644 --- a/Zotlabs/Lib/Connect.php +++ b/Zotlabs/Lib/Connect.php @@ -146,7 +146,7 @@ class Connect { } - $allowed = ((in_array($xchan['xchan_network'],['rss','zot','zot6'])) ? 1 : 0); + $allowed = ((in_array($xchan['xchan_network'],['rss', 'zot6'])) ? 1 : 0); $hookdata = ['channel_id' => $uid, 'follow_address' => $url, 'xchan' => $xchan, 'allowed' => $allowed, 'singleton' => 0]; call_hooks('follow_allow',$hookdata); @@ -261,7 +261,8 @@ class Connect { 'abook_feed' => intval(($xchan['xchan_network'] === 'rss') ? 1 : 0), 'abook_created' => datetime_convert(), 'abook_updated' => datetime_convert(), - 'abook_instance' => (($singleton) ? z_root() : '') + 'abook_instance' => (($singleton) ? z_root() : ''), + 'abook_role' => get_pconfig($uid, 'system', 'default_permcat', 'default') ] ); } @@ -300,7 +301,7 @@ class Connect { /** If there is a default group for this channel, add this connection to it */ if ($default_group) { - $g = AccessList::rec_byhash($uid,$default_group); + $g = AccessList::by_hash($uid,$default_group); if ($g) { AccessList::member_add($uid,'',$xchan_hash,$g['id']); } diff --git a/Zotlabs/Lib/Crypto.php b/Zotlabs/Lib/Crypto.php index f1794ae64..188c6bd81 100644 --- a/Zotlabs/Lib/Crypto.php +++ b/Zotlabs/Lib/Crypto.php @@ -87,6 +87,10 @@ class Crypto { return false; } + if (!$alg) { + $alg = 'sha256'; + } + try { $verify = openssl_verify($data, $sig, $key, $alg); } catch (Exception $e) { diff --git a/Zotlabs/Lib/DReport.php b/Zotlabs/Lib/DReport.php index 7515d3292..2263529b2 100644 --- a/Zotlabs/Lib/DReport.php +++ b/Zotlabs/Lib/DReport.php @@ -87,8 +87,7 @@ class DReport { // Is the sender one of our channels? - $c = q("select channel_id from channel where channel_hash = '%s' or channel_portable_id = '%s' limit 1", - dbesc($dr['sender']), + $c = q("select channel_id from channel where channel_hash = '%s' limit 1", dbesc($dr['sender']) ); diff --git a/Zotlabs/Lib/Enotify.php b/Zotlabs/Lib/Enotify.php index 7e33f09b8..1421c72ae 100644 --- a/Zotlabs/Lib/Enotify.php +++ b/Zotlabs/Lib/Enotify.php @@ -69,24 +69,24 @@ class Enotify { $sender_name = $product; $hostname = \App::get_hostname(); if(strpos($hostname,':')) - $hostname = substr($hostname,0,strpos($hostname,':')); + $hostname = substr($hostname, 0, strpos($hostname,':')); // Do not translate 'noreply' as it must be a legal 7-bit email address - $reply_email = get_config('system','reply_address'); + $reply_email = get_config('system', 'reply_address'); if(! $reply_email) $reply_email = 'noreply' . '@' . $hostname; - $sender_email = get_config('system','from_email'); + $sender_email = get_config('system', 'from_email'); if(! $sender_email) $sender_email = 'Administrator' . '@' . $hostname; - $sender_name = get_config('system','from_email_name'); + $sender_name = get_config('system', 'from_email_name'); if(! $sender_name) $sender_name = \Zotlabs\Lib\System::get_site_name(); - $additional_mail_header = ""; + $additional_mail_header = ''; if(array_key_exists('item', $params)) { require_once('include/conversation.php'); @@ -114,27 +114,28 @@ class Enotify { } - $always_show_in_notices = get_pconfig($recip['channel_id'],'system','always_show_in_notices'); - $vnotify = get_pconfig($recip['channel_id'],'system','vnotify'); + $always_show_in_notices = get_pconfig($recip['channel_id'], 'system', 'always_show_in_notices'); + $vnotify = get_pconfig($recip['channel_id'], 'system', 'vnotify'); $salutation = $recip['channel_name']; // e.g. "your post", "David's photo", etc. $possess_desc = t('%s <!item_type!>'); +// @@TODO: consider using switch instead of those elseif if ($params['type'] == NOTIFY_MAIL) { logger('notification: mail'); - $subject = sprintf( t('[$Projectname:Notify] New mail received at %s'),$sitename); - - $preamble = sprintf( t('%1$s sent you a new private message at %2$s.'), $sender['xchan_name'],$sitename); - $epreamble = sprintf( t('%1$s sent you %2$s.'),'[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]', '[zrl=$itemlink]' . t('a private message') . '[/zrl]'); - $sitelink = t('Please visit %s to view and/or reply to your private messages.'); - $tsitelink = sprintf( $sitelink, $siteurl . '/mail/' . $params['item']['id'] ); - $hsitelink = sprintf( $sitelink, '<a href="' . $siteurl . '/mail/' . $params['item']['id'] . '">' . $sitename . '</a>'); - $itemlink = $siteurl . '/mail/' . $params['item']['id']; + $subject = sprintf( t('[$Projectname:Notify] New direct message received at %s'), $sitename); + + $preamble = sprintf( t('%1$s sent you a new direct message at %2$s'), $sender['xchan_name'], $sitename); + $epreamble = sprintf( t('%1$s sent you %2$s.'), '[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]', '[zrl=$itemlink]' . t('a direct message') . '[/zrl]'); + $sitelink = t('Please visit %s to view and/or reply to your direct messages.'); + $tsitelink = sprintf( $sitelink, $siteurl . '/hq/' . gen_link_id($params['item']['mid'])); + $hsitelink = sprintf( $sitelink, '<a href="' . $siteurl . '/hq/' . gen_link_id($params['item']['mid']) . '">' . $sitename . '</a>'); + $itemlink = $siteurl . '/hq/' . gen_link_id($params['item']['mid']); } - if ($params['type'] == NOTIFY_COMMENT) { + elseif ($params['type'] === NOTIFY_COMMENT) { //logger("notification: params = " . print_r($params, true), LOGGER_DEBUG); $moderated = (($params['item']['item_blocked'] == ITEM_MODERATED) ? true : false); @@ -171,7 +172,6 @@ class Enotify { // Check to see if there was already a notify for this post. // If so don't create a second notification - $p = null; $p = q("select id from notify where link = '%s' and uid = %d limit 1", dbesc($params['link']), intval($recip['channel_id']) @@ -196,6 +196,7 @@ class Enotify { xchan_query($p); +//@@FIXME $p can be null (line 188) $item_post_type = item_post_type($p[0]); // $private = $p[0]['item_private']; $parent_id = $p[0]['id']; @@ -237,7 +238,7 @@ class Enotify { $subject = sprintf( t('[$Projectname:Notify] Moderated Comment to conversation #%1$d by %2$s'), $parent_id, $sender['xchan_name']); else $subject = sprintf( t('[$Projectname:Notify] Comment to conversation #%1$d by %2$s'), $parent_id, $sender['xchan_name']); - $preamble = sprintf( t('%1$s commented on an item/conversation you have been following.'), $sender['xchan_name']); + $preamble = sprintf( t('%1$s commented on an item/conversation you have been following'), $sender['xchan_name']); $epreamble = $dest_str; $sitelink = t('Please visit %s to view and/or reply to the conversation.'); @@ -250,12 +251,12 @@ class Enotify { } - if ($params['type'] == NOTIFY_LIKE) { + elseif ($params['type'] === NOTIFY_LIKE) { // logger("notification: params = " . print_r($params, true), LOGGER_DEBUG); $itemlink = $params['link']; - if (array_key_exists('item',$params) && (! activity_match($params['item']['verb'],ACTIVITY_LIKE))) { + if (array_key_exists('item',$params) && activity_match($params['item']['verb'],ACTIVITY_LIKE)) { if(! $always_show_in_notices || !($vnotify & VNOTIFY_LIKE)) { logger('notification: not a visible activity. Ignoring.'); pop_lang(); @@ -268,7 +269,6 @@ class Enotify { // Check to see if there was already a notify for this post. // If so don't create a second notification - $p = null; $p = q("select id from notify where link = '%s' and uid = %d limit 1", dbesc($params['link']), intval($recip['channel_id']) @@ -293,7 +293,7 @@ class Enotify { xchan_query($p); - +//@@FIXME $p can be null (line 285) $item_post_type = item_post_type($p[0]); // $private = $p[0]['item_private']; $parent_id = $p[0]['id']; @@ -302,7 +302,7 @@ class Enotify { // "your post" - if($p[0]['owner']['xchan_name'] == $p[0]['author']['xchan_name'] && intval($p[0]['item_wall'])) + if($p[0]['owner']['xchan_name'] === $p[0]['author']['xchan_name'] && intval($p[0]['item_wall'])) $dest_str = sprintf(t('%1$s liked [zrl=%2$s]your %3$s[/zrl]'), '[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]', $itemlink, @@ -318,7 +318,7 @@ class Enotify { // differents subjects for messages on the same thread. $subject = sprintf( t('[$Projectname:Notify] Like received to conversation #%1$d by %2$s'), $parent_id, $sender['xchan_name']); - $preamble = sprintf( t('%1$s liked an item/conversation you created.'), $sender['xchan_name']); + $preamble = sprintf( t('%1$s liked an item/conversation you created'), $sender['xchan_name']); $epreamble = $dest_str; $sitelink = t('Please visit %s to view and/or reply to the conversation.'); @@ -328,7 +328,7 @@ class Enotify { - if($params['type'] == NOTIFY_WALL) { + elseif($params['type'] === NOTIFY_WALL) { $subject = sprintf( t('[$Projectname:Notify] %s posted to your profile wall') , $sender['xchan_name']); $preamble = sprintf( t('%1$s posted to your profile wall at %2$s') , $sender['xchan_name'], $sitename); @@ -343,9 +343,8 @@ class Enotify { $itemlink = $params['link']; } - if ($params['type'] == NOTIFY_TAGSELF) { + elseif ($params['type'] === NOTIFY_TAGSELF) { - $p = null; $p = q("select id from notify where link = '%s' and uid = %d limit 1", dbesc($params['link']), intval($recip['channel_id']) @@ -368,7 +367,7 @@ class Enotify { $itemlink = $params['link']; } - if ($params['type'] == NOTIFY_POKE) { + elseif ($params['type'] === NOTIFY_POKE) { $subject = sprintf( t('[$Projectname:Notify] %1$s poked you') , $sender['xchan_name']); $preamble = sprintf( t('%1$s poked you at %2$s') , $sender['xchan_name'], $sitename); $epreamble = sprintf( t('%1$s [zrl=%2$s]poked you[/zrl].') , @@ -385,7 +384,7 @@ class Enotify { $itemlink = $params['link']; } - if ($params['type'] == NOTIFY_TAGSHARE) { + elseif ($params['type'] === NOTIFY_TAGSHARE) { $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]') , @@ -398,7 +397,7 @@ class Enotify { $itemlink = $params['link']; } - if ($params['type'] == NOTIFY_INTRO) { + elseif ($params['type'] === NOTIFY_INTRO) { $subject = sprintf( t('[$Projectname:Notify] Introduction received')); $preamble = sprintf( t('You\'ve received an new connection request from \'%1$s\' at %2$s'), $sender['xchan_name'], $sitename); $epreamble = sprintf( t('You\'ve received [zrl=%1$s]a new connection request[/zrl] from %2$s.'), @@ -412,7 +411,7 @@ class Enotify { $itemlink = $params['link']; } - if ($params['type'] == NOTIFY_SUGGEST) { + elseif ($params['type'] === NOTIFY_SUGGEST) { $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.'), @@ -430,11 +429,11 @@ class Enotify { $itemlink = $params['link']; } - if ($params['type'] == NOTIFY_CONFIRM) { + elseif ($params['type'] === NOTIFY_CONFIRM) { // ? } - if ($params['type'] == NOTIFY_SYSTEM) { + elseif ($params['type'] === NOTIFY_SYSTEM) { // ? } @@ -477,7 +476,7 @@ class Enotify { } while ($dups === true); - $datarray = array(); + $datarray = []; $datarray['hash'] = $hash; $datarray['sender_hash'] = $sender['xchan_hash']; $datarray['xname'] = $sender['xchan_name']; @@ -514,7 +513,7 @@ class Enotify { // (probably would be better that way) if (!$always_show_in_notices) { - if (($params['type'] == NOTIFY_WALL) || ($params['type'] == NOTIFY_MAIL) || ($params['type'] == NOTIFY_INTRO)) { + if (($params['type'] === NOTIFY_WALL) || ($params['type'] === NOTIFY_MAIL) || ($params['type'] === NOTIFY_INTRO)) { $seen = 1; } } @@ -550,12 +549,12 @@ class Enotify { } $itemlink = z_root() . '/notify/view/' . $notify_id; - $msg = str_replace('$itemlink',$itemlink,$epreamble); + $msg = str_replace('$itemlink', $itemlink, $epreamble); // wretched hack, but we don't want to duplicate all the preamble variations and we also don't want to screw up a translation if ((\App::$language === 'en' || (! \App::$language)) && strpos($msg,', ')) - $msg = substr($msg,strpos($msg,', ')+1); + $msg = substr($msg, strpos($msg,', ')+1); $datarray['id'] = $notify_id; $datarray['msg'] = $msg; @@ -575,7 +574,7 @@ class Enotify { logger('notification: sending notification email'); - $hn = get_pconfig($recip['channel_id'],'system','email_notify_host'); + $hn = get_pconfig($recip['channel_id'], 'system', 'email_notify_host'); if($hn && (! stristr(\App::get_hostname(),$hn))) { // this isn't the email notification host pop_lang(); @@ -584,7 +583,7 @@ class Enotify { $textversion = strip_tags(html_entity_decode(bbcode(stripslashes(str_replace(array("\\r", "\\n"), array( "", "\n"), $body))),ENT_QUOTES,'UTF-8')); - $htmlversion = bbcode(stripslashes(str_replace(array("\\r","\\n"), array("","<br />\n"),$body))); + $htmlversion = bbcode(stripslashes(str_replace(array("\\r","\\n"), array('',"<br />\n"),$body))); // use $_SESSION['zid_override'] to force zid() to use @@ -601,7 +600,7 @@ class Enotify { unset($_SESSION['zid_override']); unset($_SESSION['zrl_override']); - $datarray = array(); + $datarray = []; $datarray['banner'] = $banner; $datarray['product'] = $product; $datarray['preamble'] = $preamble; @@ -758,9 +757,9 @@ class Enotify { $messageSubject = email_header_encode(html_entity_decode($params['messageSubject'],ENT_QUOTES,'UTF-8'),'UTF-8'); // generate a mime boundary - $mimeBoundary = rand(0, 9) . "-" - .rand(100000000, 999999999) . "-" - .rand(100000000, 999999999) . "=:" + $mimeBoundary = rand(0, 9) . '-' + .rand(100000000, 999999999) . '-' + .rand(100000000, 999999999) . '=:' .rand(10000, 99999); // generate a multipart/alternative message header @@ -768,7 +767,7 @@ class Enotify { $params['additionalMailHeader'] . "From: $fromName <{$params['fromEmail']}>" . PHP_EOL . "Reply-To: $fromName <{$params['replyTo']}>" . PHP_EOL . - "MIME-Version: 1.0" . PHP_EOL . + 'MIME-Version: 1.0' . PHP_EOL . "Content-Type: multipart/alternative; boundary=\"{$mimeBoundary}\""; // assemble the final multipart message body with the text and html types included @@ -776,15 +775,15 @@ class Enotify { $htmlBody = chunk_split(base64_encode($params['htmlVersion'])); $multipartMessageBody = - "--" . $mimeBoundary . PHP_EOL . // plain text section - "Content-Type: text/plain; charset=UTF-8" . PHP_EOL . - "Content-Transfer-Encoding: base64" . PHP_EOL . PHP_EOL . + '--' . $mimeBoundary . PHP_EOL . // plain text section + 'Content-Type: text/plain; charset=UTF-8' . PHP_EOL . + 'Content-Transfer-Encoding: base64' . PHP_EOL . PHP_EOL . $textBody . PHP_EOL . - "--" . $mimeBoundary . PHP_EOL . // text/html section - "Content-Type: text/html; charset=UTF-8" . PHP_EOL . - "Content-Transfer-Encoding: base64" . PHP_EOL . PHP_EOL . + '--' . $mimeBoundary . PHP_EOL . // text/html section + 'Content-Type: text/html; charset=UTF-8' . PHP_EOL . + 'Content-Transfer-Encoding: base64' . PHP_EOL . PHP_EOL . $htmlBody . PHP_EOL . - "--" . $mimeBoundary . "--" . PHP_EOL; // message ending + '--' . $mimeBoundary . '--' . PHP_EOL; // message ending // send the message $res = mail( @@ -793,7 +792,7 @@ class Enotify { $multipartMessageBody, // message body $messageHeader // message headers ); - logger("notification: enotify::send returns " . (($res) ? 'success' : 'failure'), LOGGER_DEBUG); + logger('notification: enotify::send returns ' . (($res) ? 'success' : 'failure'), LOGGER_DEBUG); return $res; } @@ -833,13 +832,12 @@ class Enotify { $edit = false; if($item['edited'] > $item['created']) { + $edit = true; if($item['item_thread_top']) { $itemem_text = sprintf( t('edited a post dated %s'), relative_date($item['created'])); - $edit = true; } else { $itemem_text = sprintf( t('edited a comment dated %s'), relative_date($item['created'])); - $edit = true; } } @@ -847,6 +845,10 @@ class Enotify { // convert this logic into a json array just like the system notifications $who = (($item['verb'] === ACTIVITY_SHARE) ? 'owner' : 'author'); + $body = html2plain(bbcode($item['body'], ['drop_media']), 75, true); + if ($body) { + $body = htmlentities($body, ENT_QUOTES, 'UTF-8', false); + } $x = array( 'notify_link' => $item['llink'], @@ -856,18 +858,18 @@ 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']), 75, true), ENT_QUOTES, 'UTF-8', false), + 'body' => $body, // these are for the superblock addon 'hash' => $item[$who]['xchan_hash'], 'uid' => $item['uid'], 'display' => true ); - call_hooks('enotify_format',$x); + call_hooks('enotify_format', $x); if(! $x['display']) { return []; } @@ -884,9 +886,9 @@ class Enotify { $mid = basename($tt['link']); - $b64mid = ((strpos($mid, 'b64.') === 0) ? $mid : 'b64.' . base64url_encode($mid)); + $b64mid = gen_link_id($mid); $x = [ - 'notify_link' => z_root() . '/notify/view/' . $tt['id'], + 'notify_link' => (($tt['ntype'] === NOTIFY_MAIL) ? $tt['link'] : z_root() . '/notify/view/' . $tt['id']), 'name' => $tt['xname'], 'url' => $tt['url'], 'photo' => $tt['photo'], @@ -903,8 +905,8 @@ class Enotify { static public function format_intros($rr) { - $x = [ - 'notify_link' => z_root() . '/connections/ifpending', + return [ + 'notify_link' => z_root() . '/connections#' . $rr['abook_id'], 'name' => $rr['xchan_name'], 'addr' => $rr['xchan_addr'], 'url' => $rr['xchan_url'], @@ -914,13 +916,11 @@ class Enotify { 'message' => t('added your channel') ]; - return $x; - } static public function format_files($rr) { - $x = [ + return [ 'notify_link' => z_root() . '/sharedwithme', 'name' => $rr['author']['xchan_name'], 'addr' => $rr['author']['xchan_addr'], @@ -931,13 +931,11 @@ class Enotify { 'message' => t('shared a file with you') ]; - return $x; - } static public function format_mail($rr) { - $x = [ + return [ 'notify_link' => z_root() . '/mail/' . $rr['id'], 'name' => $rr['xchan_name'], 'addr' => $rr['xchan_addr'], @@ -945,11 +943,9 @@ class Enotify { 'photo' => $rr['xchan_photo_s'], 'when' => datetime_convert('UTC', date_default_timezone_get(), $rr['created']), 'hclass' => (intval($rr['mail_seen']) ? 'notify-seen' : 'notify-unseen'), - 'message' => t('sent you a private message'), + 'message' => t('sent you a direct message'), ]; - return $x; - } static public function format_all_events($rr) { @@ -959,7 +955,7 @@ class Enotify { $today = ((substr($strt, 0, 10) === datetime_convert('UTC', date_default_timezone_get(), 'now', 'Y-m-d')) ? true : false); $when = day_translate(datetime_convert('UTC', (($rr['adjust']) ? date_default_timezone_get() : 'UTC'), $rr['dtstart'], $bd_format)) . (($today) ? ' ' . t('[today]') : ''); - $x = [ + return [ 'notify_link' => z_root() . '/cdav/calendar/' . $rr['event_hash'], 'name' => $rr['xchan_name'], 'addr' => $rr['xchan_addr'], @@ -970,13 +966,12 @@ class Enotify { 'message' => t('created an event') ]; - return $x; } static public function format_register($rr) { - $x = [ + return [ 'notify_link' => z_root() . '/admin/accounts', 'name' => $rr['reg_did2'], //'addr' => '', @@ -986,7 +981,5 @@ class Enotify { 'message' => t('status verified') ]; - return $x; - } } diff --git a/Zotlabs/Lib/Group.php b/Zotlabs/Lib/Group.php deleted file mode 100644 index a4ff4fced..000000000 --- a/Zotlabs/Lib/Group.php +++ /dev/null @@ -1,405 +0,0 @@ -<?php - -namespace Zotlabs\Lib; - -use Zotlabs\Lib\Libsync; - - -class Group { - - static function add($uid,$name,$public = 0) { - - $ret = false; - if(x($uid) && x($name)) { - $r = self::byname($uid,$name); // check for dups - if($r !== false) { - - // This could be a problem. - // Let's assume we've just created a group which we once deleted - // all the old members are gone, but the group remains so we don't break any security - // access lists. What we're doing here is reviving the dead group, but old content which - // was restricted to this group may now be seen by the new group members. - - $z = q("SELECT * FROM pgrp WHERE id = %d LIMIT 1", - intval($r) - ); - if(($z) && $z[0]['deleted']) { - q('UPDATE pgrp SET deleted = 0 WHERE id = %d', intval($z[0]['id'])); - notice( t('A deleted group with this name was revived. Existing item permissions <strong>may</strong> apply to this group and any future members. If this is not what you intended, please create another group with a different name.') . EOL); - } - return true; - } - - do { - $dups = false; - $hash = random_string(32) . str_replace(['<','>'],['.','.'], $name); - - $r = q("SELECT id FROM pgrp WHERE hash = '%s' LIMIT 1", dbesc($hash)); - if($r) - $dups = true; - } while($dups == true); - - - $r = q("INSERT INTO pgrp ( hash, uid, visible, gname ) - VALUES( '%s', %d, %d, '%s' ) ", - dbesc($hash), - intval($uid), - intval($public), - dbesc($name) - ); - $ret = $r; - } - - Libsync::build_sync_packet($uid,null,true); - return $ret; - } - - - static function remove($uid,$name) { - $ret = false; - if(x($uid) && x($name)) { - $r = q("SELECT id, hash FROM pgrp WHERE uid = %d AND gname = '%s' LIMIT 1", - intval($uid), - dbesc($name) - ); - if($r) { - $group_id = $r[0]['id']; - $group_hash = $r[0]['hash']; - } - - if(! $group_id) - return false; - - // remove group from default posting lists - $r = q("SELECT channel_default_group, channel_allow_gid, channel_deny_gid FROM channel WHERE channel_id = %d LIMIT 1", - intval($uid) - ); - if($r) { - $user_info = $r[0]; - $change = false; - - if($user_info['channel_default_group'] == $group_hash) { - $user_info['channel_default_group'] = ''; - $change = true; - } - if(strpos($user_info['channel_allow_gid'], '<' . $group_hash . '>') !== false) { - $user_info['channel_allow_gid'] = str_replace('<' . $group_hash . '>', '', $user_info['channel_allow_gid']); - $change = true; - } - if(strpos($user_info['channel_deny_gid'], '<' . $group_hash . '>') !== false) { - $user_info['channel_deny_gid'] = str_replace('<' . $group_hash . '>', '', $user_info['channel_deny_gid']); - $change = true; - } - - if($change) { - q("UPDATE channel SET channel_default_group = '%s', channel_allow_gid = '%s', channel_deny_gid = '%s' - WHERE channel_id = %d", - intval($user_info['channel_default_group']), - dbesc($user_info['channel_allow_gid']), - dbesc($user_info['channel_deny_gid']), - intval($uid) - ); - } - } - - // remove all members - $r = q("DELETE FROM pgrp_member WHERE uid = %d AND gid = %d ", - intval($uid), - intval($group_id) - ); - - // remove group - $r = q("UPDATE pgrp SET deleted = 1 WHERE uid = %d AND gname = '%s'", - intval($uid), - dbesc($name) - ); - - $ret = $r; - - } - - Libsync::build_sync_packet($uid,null,true); - - return $ret; - } - - - static function byname($uid,$name) { - if((! $uid) || (! strlen($name))) - return false; - $r = q("SELECT * FROM pgrp WHERE uid = %d AND gname = '%s' LIMIT 1", - intval($uid), - dbesc($name) - ); - if($r) - return $r[0]['id']; - return false; - } - - - static function rec_byhash($uid,$hash) { - if((! $uid) || (! strlen($hash))) - return false; - $r = q("SELECT * FROM pgrp WHERE uid = %d AND hash = '%s' LIMIT 1", - intval($uid), - dbesc($hash) - ); - if($r) - return $r[0]; - return false; - } - - - static function member_remove($uid,$name,$member) { - $gid = self::byname($uid,$name); - if(! $gid) - return false; - if(! ( $uid && $gid && $member)) - return false; - $r = q("DELETE FROM pgrp_member WHERE uid = %d AND gid = %d AND xchan = '%s' ", - intval($uid), - intval($gid), - dbesc($member) - ); - - Libsync::build_sync_packet($uid,null,true); - - return $r; - } - - - static function member_add($uid,$name,$member,$gid = 0) { - if(! $gid) - $gid = self::byname($uid,$name); - if((! $gid) || (! $uid) || (! $member)) - return false; - - $r = q("SELECT * FROM pgrp_member WHERE uid = %d AND gid = %d AND xchan = '%s' LIMIT 1", - intval($uid), - intval($gid), - dbesc($member) - ); - if($r) - return true; // You might question this, but - // we indicate success because the group member was in fact created - // -- It was just created at another time - if(! $r) - $r = q("INSERT INTO pgrp_member (uid, gid, xchan) - VALUES( %d, %d, '%s' ) ", - intval($uid), - intval($gid), - dbesc($member) - ); - - Libsync::build_sync_packet($uid,null,true); - - return $r; - } - - - static function members($gid) { - $ret = array(); - if(intval($gid)) { - $r = q("SELECT * FROM pgrp_member - LEFT JOIN abook ON abook_xchan = pgrp_member.xchan left join xchan on xchan_hash = abook_xchan - WHERE gid = %d AND abook_channel = %d and pgrp_member.uid = %d and xchan_deleted = 0 and abook_self = 0 and abook_blocked = 0 and abook_pending = 0 ORDER BY xchan_name ASC ", - intval($gid), - intval(local_channel()), - intval(local_channel()) - ); - if($r) - $ret = $r; - } - return $ret; - } - - static function members_xchan($gid) { - $ret = []; - if(intval($gid)) { - $r = q("SELECT xchan FROM pgrp_member WHERE gid = %d AND uid = %d", - intval($gid), - intval(local_channel()) - ); - if($r) { - foreach($r as $rr) { - $ret[] = $rr['xchan']; - } - } - } - return $ret; - } - - static function members_profile_xchan($uid,$gid) { - $ret = []; - - if(intval($gid)) { - $r = q("SELECT abook_xchan as xchan from abook left join profile on abook_profile = profile_guid where profile.id = %d and profile.uid = %d", - intval($gid), - intval($uid) - ); - if($r) { - foreach($r as $rr) { - $ret[] = $rr['xchan']; - } - } - } - return $ret; - } - - - - - static function select($uid,$group = '') { - - $grps = []; - $o = ''; - - $r = q("SELECT * FROM pgrp WHERE deleted = 0 AND uid = %d ORDER BY gname ASC", - intval($uid) - ); - $grps[] = array('name' => '', 'hash' => '0', 'selected' => ''); - if($r) { - foreach($r as $rr) { - $grps[] = array('name' => $rr['gname'], 'id' => $rr['hash'], 'selected' => (($group == $rr['hash']) ? 'true' : '')); - } - - } - logger('select: ' . print_r($grps,true), LOGGER_DATA); - - $o = replace_macros(get_markup_template('group_selection.tpl'), array( - '$label' => t('Add new connections to this privacy group'), - '$groups' => $grps - )); - return $o; - } - - - - - static function widget($every="connections",$each="group",$edit = false, $group_id = 0, $cid = '',$mode = 1) { - - $o = ''; - - if(! (local_channel() && feature_enabled(local_channel(),'groups'))) { - return ''; - } - - $groups = array(); - - $r = q("SELECT * FROM pgrp WHERE deleted = 0 AND uid = %d ORDER BY gname ASC", - intval($_SESSION['uid']) - ); - $member_of = array(); - if($cid) { - $member_of = self::containing(local_channel(),$cid); - } - - if($r) { - foreach($r as $rr) { - $selected = (($group_id == $rr['id']) ? ' group-selected' : ''); - - if ($edit) { - $groupedit = [ 'href' => "group/".$rr['id'], 'title' => t('edit') ]; - } - else { - $groupedit = null; - } - - $groups[] = [ - 'id' => $rr['id'], - 'enc_cid' => base64url_encode($cid), - 'cid' => $cid, - 'text' => $rr['gname'], - 'selected' => $selected, - 'href' => (($mode == 0) ? $each.'?f=&gid='.$rr['id'] : $each."/".$rr['id']) . ((x($_GET,'new')) ? '&new=' . $_GET['new'] : '') . ((x($_GET,'order')) ? '&order=' . $_GET['order'] : ''), - 'edit' => $groupedit, - 'ismember' => in_array($rr['id'],$member_of), - ]; - } - } - - - $tpl = get_markup_template("group_side.tpl"); - $o = replace_macros($tpl, array( - '$title' => t('Privacy Groups'), - '$edittext' => t('Edit group'), - '$createtext' => t('Add privacy group'), - '$ungrouped' => (($every === 'contacts') ? t('Channels not in any privacy group') : ''), - '$groups' => $groups, - '$add' => t('add'), - )); - - - return $o; - } - - - static function expand($g) { - if(! (is_array($g) && count($g))) - return array(); - - $ret = []; - $x = []; - - // private profile linked virtual groups - - foreach($g as $gv) { - if(substr($gv,0,3) === 'vp.') { - $profile_hash = substr($gv,3); - if($profile_hash) { - $r = q("select abook_xchan from abook where abook_profile = '%s'", - dbesc($profile_hash) - ); - if($r) { - foreach($r as $rv) { - $ret[] = $rv['abook_xchan']; - } - } - } - } - else { - $x[] = $gv; - } - } - - if($x) { - stringify_array_elms($x,true); - $groups = implode(',', $x); - if($groups) { - $r = q("SELECT xchan FROM pgrp_member WHERE gid IN ( select id from pgrp where hash in ( $groups ))"); - if($r) { - foreach($r as $rr) { - $ret[] = $rr['xchan']; - } - } - } - } - return $ret; - } - - - static function member_of($c) { - $r = q("SELECT pgrp.gname, pgrp.id FROM pgrp LEFT JOIN pgrp_member ON pgrp_member.gid = pgrp.id WHERE pgrp_member.xchan = '%s' AND pgrp.deleted = 0 ORDER BY pgrp.gname ASC ", - dbesc($c) - ); - - return $r; - - } - - static function containing($uid,$c) { - - $r = q("SELECT gid FROM pgrp_member WHERE uid = %d AND pgrp_member.xchan = '%s' ", - intval($uid), - dbesc($c) - ); - - $ret = array(); - if($r) { - foreach($r as $rr) - $ret[] = $rr['gid']; - } - - return $ret; - } -}
\ No newline at end of file diff --git a/Zotlabs/Lib/LDSignatures.php b/Zotlabs/Lib/LDSignatures.php index 1c2095f10..00042bc7a 100644 --- a/Zotlabs/Lib/LDSignatures.php +++ b/Zotlabs/Lib/LDSignatures.php @@ -75,22 +75,23 @@ class LDSignatures { } static function hash($obj) { - - return hash('sha256',self::normalise($obj)); + return hash('sha256', self::normalise($obj)); } static function normalise($data) { + $ret = ''; + if(is_string($data)) { $data = json_decode($data); } if(! is_object($data)) - return ''; + return $ret; jsonld_set_document_loader('jsonld_document_loader'); try { - $d = jsonld_normalize($data,[ 'algorithm' => 'URDNA2015', 'format' => 'application/nquads' ]); + $ret = jsonld_normalize($data,[ 'algorithm' => 'URDNA2015', 'format' => 'application/nquads' ]); } catch (\Exception $e) { // Don't log the exception - this can exhaust memory @@ -98,7 +99,7 @@ class LDSignatures { logger('normalise error: ' . print_r($data,true)); } - return $d; + return $ret; } static function salmon_sign($data,$channel) { diff --git a/Zotlabs/Lib/Libsync.php b/Zotlabs/Lib/Libsync.php index 5455aa2ea..36a0a044c 100644 --- a/Zotlabs/Lib/Libsync.php +++ b/Zotlabs/Lib/Libsync.php @@ -141,7 +141,6 @@ class Libsync { logger('Packet: ' . print_r($info, true), LOGGER_DATA, LOG_DEBUG); $total = count($synchubs); - foreach ($synchubs as $hub) { $hash = random_string(); $n = Libzot::build_packet($channel, 'sync', $env_recips, json_encode($info), 'hz', $hub['hubloc_sitekey'], $hub['site_crypto']); @@ -186,7 +185,6 @@ class Libsync { require_once('include/import.php'); $result = []; - $keychange = ((array_key_exists('keychange', $arr)) ? true : false); foreach ($deliveries as $d) { @@ -232,8 +230,35 @@ class Libsync { if (array_key_exists('config', $arr) && is_array($arr['config']) && count($arr['config'])) { foreach ($arr['config'] as $cat => $k) { - foreach ($arr['config'][$cat] as $k => $v) - set_pconfig($channel['channel_id'], $cat, $k, $v); + $pconfig_updated = []; + + foreach($arr['config'][$cat] as $k => $v) { + if ($cat === 'hz_delpconfig' && strpos($k, 'b64.') === 0) { + $delpconfig = explode(':', unpack_link_id($k)); + + // delete the provided pconfig + del_pconfig($channel['channel_id'], $delpconfig[0], $delpconfig[1], $v); + + // delete the messenger pconfig + del_pconfig($channel['channel_id'], 'hz_delpconfig', $k); + } + + if (strpos($k,'pcfgud:') === 0) { + $realk = substr($k,7); + $pconfig_updated[$realk] = $v; + unset($arr['config'][$cat][$k]); + } + } + + foreach($arr['config'][$cat] as $k => $v) { + if (!isset($pconfig_updated[$k])) { + $pconfig_updated[$k] = NULL; + } + + if ($cat !== 'hz_delpconfig') { + set_pconfig($channel['channel_id'],$cat,$k,$v,$pconfig_updated[$k]); + } + } } } @@ -246,6 +271,10 @@ class Libsync { if (array_key_exists('app', $arr) && $arr['app']) sync_apps($channel, $arr['app']); + if (array_key_exists('sysapp',$arr) && $arr['sysapp']) { + sync_sysapps($channel, $arr['sysapp']); + } + if (array_key_exists('addressbook', $arr) && $arr['addressbook']) sync_addressbook($channel, $arr['addressbook']); @@ -255,11 +284,8 @@ class Libsync { if (array_key_exists('chatroom', $arr) && $arr['chatroom']) sync_chatrooms($channel, $arr['chatroom']); - if (array_key_exists('conv', $arr) && $arr['conv']) - import_conv($channel, $arr['conv']); - - if (array_key_exists('mail', $arr) && $arr['mail']) - sync_mail($channel, $arr['mail']); + //if (array_key_exists('mail', $arr) && $arr['mail']) + // sync_mail($channel, $arr['mail']); if (array_key_exists('event', $arr) && $arr['event']) sync_events($channel, $arr['event']); @@ -273,8 +299,8 @@ class Libsync { // deprecated, maintaining for a few months for upward compatibility // this should sync webpages, but the logic is a bit subtle - if (array_key_exists('item_id', $arr) && $arr['item_id']) - sync_items($channel, $arr['item_id']); + //if (array_key_exists('item_id', $arr) && $arr['item_id']) + // sync_items($channel, $arr['item_id']); if (array_key_exists('menu', $arr) && $arr['menu']) sync_menus($channel, $arr['menu']); @@ -385,19 +411,42 @@ class Libsync { // This relies on the undocumented behaviour that red sites send xchan info with the abook // and import_author_xchan will look them up on all federated networks - if ($abook['abook_xchan'] && $abook['xchan_addr']) { + $found = false; + if ($abook['abook_xchan'] && $abook['xchan_addr'] && (! in_array($abook['xchan_network'], [ 'token', 'unknown' ]))) { $h = Libzot::get_hublocs($abook['abook_xchan']); - if (!$h) { + if ($h) { + $found = true; + } + else { $xhash = import_author_xchan(encode_item_xchan($abook)); - if (!$xhash) { + if ($xhash) { + $found = true; + } + else { logger('Import of ' . $abook['xchan_addr'] . ' failed.'); - continue; } } } + if (!$found && !in_array($abook['xchan_network'], ['zot6', 'activitypub', 'diaspora'])) { + // just import the record. + $xc = []; + foreach ($abook as $k => $v) { + if (strpos($k,'xchan_') === 0) { + $xc[$k] = $v; + } + } + $r = q("select * from xchan where xchan_hash = '%s'", + dbesc($xc['xchan_hash']) + ); + if (! $r) { + xchan_store_lowlevel($xc); + } + } + + foreach ($abook as $k => $v) { - if (in_array($k, $disallowed) || (strpos($k, 'abook') !== 0)) { + if (in_array($k, $disallowed) || (strpos($k, 'abook_') !== 0)) { continue; } if (!in_array($k, $fields)) { @@ -411,6 +460,13 @@ class Libsync { if (array_key_exists('abook_instance', $clean) && $clean['abook_instance'] && strpos($clean['abook_instance'], z_root()) === false) { $clean['abook_not_here'] = 1; + + // guest pass or access token - don't try to probe since it is one-way + // we are relying on the undocumented behaviour that the abook record also contains the xchan + if ($abook['xchan_network'] === 'token') { + $clean['abook_instance'] .= ','; + $clean['abook_instance'] .= z_root(); + } } @@ -707,6 +763,15 @@ class Libsync { $ret = []; + // If a sender reports that the channel has been deleted, delete its hubloc + if (isset($arr['deleted_locally']) && intval($arr['deleted_locally'])) { + q("UPDATE hubloc SET hubloc_deleted = 1, hubloc_updated = '%s' WHERE hubloc_hash = '%s' AND hubloc_url = '%s'", + dbesc(datetime_convert()), + dbesc($sender['hash']), + dbesc($sender['site']['url']) + ); + } + if ($arr['locations']) { if ($absolute) @@ -760,14 +825,13 @@ class Libsync { // match as many fields as possible in case anything at all changed. - $r = q("select * from hubloc where hubloc_hash = '%s' and hubloc_guid = '%s' and hubloc_guid_sig = '%s' and hubloc_id_url = '%s' and hubloc_url = '%s' and hubloc_url_sig = '%s' and hubloc_site_id = '%s' and hubloc_host = '%s' and hubloc_addr = '%s' and hubloc_callback = '%s' and hubloc_sitekey = '%s' ", + $r = q("select * from hubloc where hubloc_hash = '%s' and hubloc_guid = '%s' and hubloc_guid_sig = '%s' and hubloc_id_url = '%s' and hubloc_url = '%s' and hubloc_url_sig = '%s' and hubloc_host = '%s' and hubloc_addr = '%s' and hubloc_callback = '%s' and hubloc_sitekey = '%s' ", dbesc($sender['hash']), dbesc($sender['id']), dbesc($sender['id_sig']), dbesc($location['id_url']), dbesc($location['url']), dbesc($location['url_sig']), - dbesc($location['site_id']), dbesc($location['host']), dbesc($location['address']), dbesc($location['callback']), @@ -776,6 +840,15 @@ class Libsync { if ($r) { logger('Hub exists: ' . $location['url'], LOGGER_DEBUG); + // generate a new hubloc_site_id if it's wrong due to historical bugs 2021-11-30 + + if ($r[0]['hubloc_site_id'] !== $location['site_id']) { + q("update hubloc set hubloc_site_id = '%s' where hubloc_id = %d", + dbesc(Libzot::make_xchan_hash($location['url'], $location['sitekey'])), + intval($r[0]['hubloc_id']) + ); + } + // update connection timestamp if this is the site we're talking to // This only happens when called from import_xchan @@ -864,6 +937,7 @@ class Libsync { $what .= 'delete_hub '; $changed = true; } + continue; } diff --git a/Zotlabs/Lib/Libzot.php b/Zotlabs/Lib/Libzot.php index c7d001d21..2ed18d10b 100644 --- a/Zotlabs/Lib/Libzot.php +++ b/Zotlabs/Lib/Libzot.php @@ -266,7 +266,7 @@ class Libzot { dbesc($them['xchan_addr']) ); } - if (!$r) { + if (!$r && array_key_exists('xchan_hash', $them) && $them['xchan_hash']) { $r = q("select hubloc_id_url, hubloc_primary from hubloc where hubloc_hash = '%s' order by hubloc_id desc", dbesc($them['xchan_hash']) ); @@ -275,8 +275,8 @@ class Libzot { if ($r) { foreach ($r as $rr) { if (intval($rr['hubloc_primary'])) { - $url = $rr['hubloc_id_url']; - $record = $rr; + $url = $rr['hubloc_id_url']; + break; } } if (!$url) { @@ -284,13 +284,17 @@ class Libzot { } } } + if (!$url) { logger('zot_refresh: no url'); return false; } + $m = parse_url($url); + $site_url = unparse_url([ 'scheme' => $m['scheme'], 'host' => $m['host'] ]); + $s = q("select site_dead from site where site_url = '%s' limit 1", - dbesc($url) + dbesc($site_url) ); if ($s && intval($s[0]['site_dead']) && (!$force)) { @@ -299,25 +303,25 @@ class Libzot { } $record = Zotfinger::exec($url, $channel); - // Check the HTTP signature + // Check the HTTP signature $hsig = $record['signature']; - if ($hsig && $hsig['signer'] === $url && $hsig['header_valid'] === true && $hsig['content_valid'] === true) + if ($hsig && $hsig['signer'] === $url && $hsig['header_valid'] === true && $hsig['content_valid'] === true) { $hsig_valid = true; + } if (!$hsig_valid) { logger('http signature not valid: ' . print_r($hsig, true)); return false; } - logger('zot-info: ' . print_r($record, true), LOGGER_DATA, LOG_DEBUG); $x = self::import_xchan($record['data'], (($force) ? UPDATE_FLAGS_FORCED : UPDATE_FLAGS_UPDATED)); - - if (!$x['success']) + if (!$x['success']) { return false; + } if ($channel && $record['data']['permissions']) { $permissions = explode(',', $record['data']['permissions']); @@ -357,8 +361,9 @@ class Libzot { // we have as we may have updated the year after sending a notification; and resetting // to the one we just received would cause us to create duplicated events. - if (substr($r[0]['abook_dob'], 5) == substr($next_birthday, 5)) + if (substr($r[0]['abook_dob'], 5) == substr($next_birthday, 5)) { $next_birthday = $r[0]['abook_dob']; + } $y = q("update abook set abook_dob = '%s' where abook_xchan = '%s' and abook_channel = %d @@ -368,20 +373,23 @@ class Libzot { intval($channel['channel_id']) ); - if (!$y) + if (!$y) { logger('abook update failed'); + } else { // if we were just granted read stream permission and didn't have it before, try to pull in some posts - if ((!$old_read_stream_perm) && (intval($permissions['view_stream']))) + if (!$old_read_stream_perm && intval($permissions['view_stream'])) { Master::Summon(['Onepoll', $r[0]['abook_id']]); + } } } else { - $p = Permissions::connect_perms($channel['channel_id']); - $my_perms = $p['perms']; + $p = Permissions::connect_perms($channel['channel_id']); + $my_perms = $p['perms']; $automatic = $p['automatic']; + $role = (($automatic) ? $p['role'] : ''); // new connection @@ -403,7 +411,8 @@ class Libzot { 'abook_created' => datetime_convert(), 'abook_updated' => datetime_convert(), 'abook_dob' => $next_birthday, - 'abook_pending' => intval(($automatic) ? 0 : 1) + 'abook_pending' => intval(($automatic) ? 0 : 1), + 'abook_role' => $role ] ); @@ -419,53 +428,62 @@ class Libzot { ); if ($new_connection) { - if (!Permissions::PermsCompare($new_perms, $previous_perms)) + if (!Permissions::PermsCompare($new_perms, $previous_perms)) { Master::Summon(['Notifier', 'permission_create', $new_connection[0]['abook_id']]); + } + Enotify::submit( [ 'type' => NOTIFY_INTRO, 'from_xchan' => $x['hash'], 'to_xchan' => $channel['channel_hash'], - 'link' => z_root() . '/connedit/' . $new_connection[0]['abook_id'] + 'link' => z_root() . '/connections#' . $new_connection[0]['abook_id'] ] ); if (intval($permissions['view_stream'])) { if (intval(get_pconfig($channel['channel_id'], 'perm_limits', 'send_stream') & PERMS_PENDING) - || (!intval($new_connection[0]['abook_pending']))) + || (!intval($new_connection[0]['abook_pending']))) { Master::Summon(['Onepoll', $new_connection[0]['abook_id']]); + } } - // If there is a default group for this channel, add this connection to it - // for pending connections this will happens at acceptance time. + // for pending connections this will happen at acceptance time. if (!intval($new_connection[0]['abook_pending'])) { $default_group = $channel['channel_default_group']; + if ($default_group) { - $g = Group::rec_byhash($channel['channel_id'], $default_group); - if ($g) - Group::member_add($channel['channel_id'], '', $x['hash'], $g['id']); + $g = AccessList::by_hash($channel['channel_id'], $default_group); + + if ($g) { + AccessList::member_add($channel['channel_id'], '', $x['hash'], $g['id']); + } } } unset($new_connection[0]['abook_id']); unset($new_connection[0]['abook_account']); unset($new_connection[0]['abook_channel']); + $abconfig = load_abconfig($channel['channel_id'], $new_connection['abook_xchan']); - if ($abconfig) + + if ($abconfig) { $new_connection['abconfig'] = $abconfig; + } Libsync::build_sync_packet($channel['channel_id'], ['abook' => $new_connection]); + } } - } return true; } return false; } + /** * @brief Look up if channel is known and previously verified. * @@ -479,6 +497,7 @@ class Libzot { * * \e string \b id_sig => id signed with conversant's private key * * \e string \b location => URL of the origination hub of this communication * * \e string \b location_sig => URL signed with conversant's private key + * * \e string \b site_id => URL signed with conversant's private key * @param boolean $multiple (optional) default false * * @return array|null @@ -488,7 +507,7 @@ class Libzot { static function gethub($arr, $multiple = false) { - if ($arr['id'] && $arr['id_sig'] && $arr['location'] && $arr['location_sig']) { + if ($arr['id'] && $arr['id_sig'] && $arr['location'] && $arr['location_sig'] && $arr['site_id']) { if (!check_siteallowed($arr['location'])) { logger('blacklisted site: ' . $arr['location']); @@ -512,9 +531,9 @@ class Libzot { logger('Found', LOGGER_DEBUG); return (($multiple) ? $r : $r[0]); } + logger('Not found: ' . print_r($arr, true), LOGGER_DEBUG); } - logger('Not found: ' . print_r($arr, true), LOGGER_DEBUG); - + logger('Incomplete array: ' . print_r($arr, true), LOGGER_DEBUG); return false; } @@ -616,7 +635,6 @@ class Libzot { */ static function import_xchan($arr, $ud_flags = UPDATE_FLAGS_UPDATED, $ud_arr = null) { - /** * @hooks import_xchan * Called when processing the result of zot_finger() to store the result @@ -658,6 +676,10 @@ class Libzot { logger('import_xchan: ' . $xchan_hash, LOGGER_DEBUG); + if (isset($arr['signing_algorithm']) && $arr['signing_algorithm']) { + set_xconfig($xchan_hash, 'system', 'signing_algorithm', $arr['signing_algorithm']); + } + $r = q("select * from xchan where xchan_hash = '%s' limit 1", dbesc($xchan_hash) ); @@ -666,6 +688,7 @@ class Libzot { $arr['connect_url'] = ''; if ($r) { + if ($arr['photo'] && array_key_exists('updated', $arr['photo']) && $arr['photo']['updated'] > $r[0]['xchan_photo_date']) $import_photos = true; @@ -984,7 +1007,7 @@ class Libzot { $x = Crypto::unencapsulate($x, get_config('system', 'prvkey')); - if (!is_array($x)) { + if ($x && !is_array($x)) { $x = json_decode($x, true); } @@ -1126,6 +1149,7 @@ class Libzot { if ($env['encoding'] === 'activitystreams') { $AS = new ActivityStreams($data); + if (!$AS->is_valid()) { logger('Activity rejected: ' . print_r($data, true)); return; @@ -1141,7 +1165,6 @@ class Libzot { } - $deliveries = null; if (array_key_exists('recipients', $env) && count($env['recipients'])) { @@ -1199,31 +1222,51 @@ class Libzot { if (in_array($env['type'], ['activity', 'response'])) { - $r = q("select hubloc_hash, hubloc_network from hubloc where hubloc_id_url = '%s' ", + if(empty($AS->actor['id'])) { + logger('No actor id!'); + return; + } + + $r = q("select hubloc_hash, hubloc_network, hubloc_url from hubloc where hubloc_id_url = '%s'", dbesc($AS->actor['id']) ); + if (! $r) { + // Author is unknown to this site. Perform channel discovery and try again. + $z = discover_by_webbie($AS->actor['id']); + if ($z) { + $r = q("select hubloc_hash, hubloc_network, hubloc_url from hubloc where hubloc_id_url = '%s'", + dbesc($AS->actor['id']) + ); + } + } + if ($r) { - // selects a zot6 hash if available, otherwise use whatever we have - $r = self::zot_record_preferred($r); + $r = self::zot_record_preferred($r); $arr['author_xchan'] = $r['hubloc_hash']; } - if (!$arr['author_xchan']) { + if (! $arr['author_xchan']) { logger('No author!'); return; } - $s = q("select hubloc_hash from hubloc where hubloc_id_url = '%s' and hubloc_network = 'zot6' limit 1", - dbesc($env['sender']) - ); + $arr['owner_xchan'] = $env['sender']; + + if(filter_var($env['sender'], FILTER_VALIDATE_URL)) { + // in individual delivery, change owner if needed + $s = q("select hubloc_hash, hubloc_url from hubloc where hubloc_id_url = '%s' and hubloc_network = 'zot6' limit 1", + dbesc($env['sender']) + ); - // in individual delivery, change owner if needed - if ($s) { - $arr['owner_xchan'] = $s[0]['hubloc_hash']; + if ($s) { + $arr['owner_xchan'] = $s[0]['hubloc_hash']; + } } - else { - $arr['owner_xchan'] = $env['sender']; + + if (! $arr['owner_xchan']) { + logger('No owner!'); + return; } if ($private && (!intval($arr['item_private']))) { @@ -1251,29 +1294,14 @@ class Libzot { if ($AS->data['hubloc']) { $arr['item_verified'] = true; + } - if (!array_key_exists('comment_policy', $arr)) { - // set comment policy depending on source hub. Unknown or osada is ActivityPub. - // Anything else we'll say is zot - which could have a range of project names - $s = q("select site_project from site where site_url = '%s' limit 1", - dbesc($r[0]['hubloc_url']) - ); - - if ((!$s) || (in_array($s[0]['site_project'], ['', 'osada']))) { - $arr['comment_policy'] = 'authenticated'; - } - else { - $arr['comment_policy'] = 'contacts'; - } - } + if (!array_key_exists('comment_policy', $arr)) { + $arr['comment_policy'] = 'authenticated'; } - if ($AS->data['signed_data']) { - IConfig::Set($arr, 'activitypub', 'signed_data', $AS->data['signed_data'], false); - $j = json_decode($AS->data['signed_data'], true); - if ($j) { - IConfig::Set($arr, 'activitypub', 'rawmsg', json_encode(JSalmon::unpack($j['data'])), true); - } + if ($AS->meta['signed_data']) { + IConfig::Set($arr, 'activitypub', 'signed_data', $AS->meta['signed_data'], false); } logger('Activity received: ' . print_r($arr, true), LOGGER_DATA, LOG_DEBUG); @@ -1332,7 +1360,7 @@ class Libzot { static function find_parent($env, $act) { if ($act) { - if (in_array($act->type, ['Like', 'Dislike'])) { + if (in_array($act->type, ['Like', 'Dislike']) && is_array($act->obj)) { return $act->obj['id']; } if ($act->parent_id) { @@ -1377,8 +1405,6 @@ class Libzot { $check_mentions = true; } } - elseif ($msg['type'] === 'mail') - $perm = 'post_mail'; $r = []; @@ -1464,10 +1490,11 @@ class Libzot { * @param boolean $relay * @param boolean $public (optional) default false * @param boolean $request (optional) default false + * @param boolean $force (optional) default false - should only be set for manual fetch * @return array */ - static function process_delivery($sender, $act, $arr, $deliveries, $relay, $public = false, $request = false) { + static function process_delivery($sender, $act, $arr, $deliveries, $relay, $public = false, $request = false, $force = false) { $result = []; @@ -1545,7 +1572,11 @@ class Libzot { $local_public = false; continue; } - if (!MessageFilter::evaluate($arr, get_config('system', 'pubstream_incl'), get_config('system', 'pubstream_excl'))) { + + $incl = get_config('system','pubstream_incl'); + $excl = get_config('system','pubstream_excl'); + + if(($incl || $excl) && !MessageFilter::evaluate($arr, $incl, $excl)) { $local_public = false; continue; } @@ -1569,7 +1600,8 @@ class Libzot { if ((!$tag_delivery) && (!$local_public)) { $allowed = (perm_is_allowed($channel['channel_id'], $sender, $perm)); - if (!$allowed) { + + if ((!$allowed) && $perm === 'post_comments') { $parent = q("select * from item where mid = '%s' and uid = %d limit 1", dbesc($arr['parent_mid']), intval($channel['channel_id']) @@ -1595,7 +1627,7 @@ class Libzot { // doesn't exist. if ($perm === 'send_stream') { - if (get_pconfig($channel['channel_id'], 'system', 'hyperdrive', false) || $arr['verb'] === ACTIVITY_SHARE) { + if ($force || get_pconfig($channel['channel_id'], 'system', 'hyperdrive', false) || $arr['verb'] === ACTIVITY_SHARE) { $allowed = true; } } @@ -1606,6 +1638,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'); @@ -1713,11 +1751,13 @@ class Libzot { } } - $ab = q("select * from abook where abook_channel = %d and abook_xchan = '%s'", + // This is used to fetch allow/deny rules if either the sender + // or owner is a connection. post_is_importable() evaluates all of them + $abook = q("select * from abook where abook_channel = %d and ( abook_xchan = '%s' OR abook_xchan = '%s' )", intval($channel['channel_id']), - dbesc($arr['owner_xchan']) + dbesc($arr['owner_xchan']), + dbesc($arr['author_xchan']) ); - $abook = (($ab) ? $ab[0] : null); if (intval($arr['item_deleted'])) { @@ -1768,17 +1808,18 @@ class Libzot { elseif ($arr['edited'] > $r[0]['edited']) { $arr['id'] = $r[0]['id']; $arr['uid'] = $channel['channel_id']; - if (($arr['mid'] == $arr['parent_mid']) && (!post_is_importable($arr, $abook))) { - $DR->update('update ignored'); - $result[] = $DR->get(); - } - else { - $item_result = self::update_imported_item($sender, $arr, $r[0], $channel['channel_id'], $tag_delivery); - $DR->update('updated'); - $result[] = $DR->get(); - if (!$relay) - add_source_route($item_id, $sender); - } + + if (post_is_importable($channel['channel_id'], $arr, $abook)) { + $item_result = self::update_imported_item($sender, $arr, $r[0], $channel['channel_id'], $tag_delivery); + $DR->update('updated'); + $result[] = $DR->get(); + if (!$relay) { + add_source_route($item_id, $sender); + } + } else { + $DR->update('update ignored'); + $result[] = $DR->get(); + } } else { $DR->update('update ignored'); @@ -1808,20 +1849,29 @@ class Libzot { $item_id = 0; - if (($arr['mid'] == $arr['parent_mid']) && (!post_is_importable($arr, $abook))) { - $DR->update('post ignored'); - $result[] = $DR->get(); + $maxlen = get_max_import_size(); + + if ($maxlen && mb_strlen($arr['body']) > $maxlen) { + $arr['body'] = mb_substr($arr['body'], 0, $maxlen, 'UTF-8'); + logger('message length exceeds max_import_size: truncated'); } - else { + + if ($maxlen && mb_strlen($arr['summary']) > $maxlen) { + $arr['summary'] = mb_substr($arr['summary'], 0, $maxlen, 'UTF-8'); + logger('message summary length exceeds max_import_size: truncated'); + } + + if (post_is_importable($arr['uid'], $arr, $abook)) { $item_result = item_store($arr); if ($item_result['success']) { $item_id = $item_result['item_id']; - $parr = [ + $parr = [ 'item_id' => $item_id, - 'item' => $arr, - 'sender' => $sender, + 'item' => $arr, + 'sender' => $sender, 'channel' => $channel ]; + /** * @hooks activity_received * Called when an activity (post, comment, like, etc.) has been received from a zot source. @@ -1831,13 +1881,19 @@ class Libzot { * * \e array \b channel */ call_hooks('activity_received', $parr); + // don't add a source route if it's a relay or later recipients will get a route mismatch - if (!$relay) + if (!$relay) { add_source_route($item_id, $sender); + } } $DR->update(($item_id) ? 'posted' : 'storage failed: ' . $item_result['message']); $result[] = $DR->get(); + } else { + $DR->update('post ignored'); + $result[] = $DR->get(); } + } // preserve conversations with which you are involved from expiration @@ -1864,7 +1920,7 @@ class Libzot { return $result; } - static public function fetch_conversation($channel, $mid) { + static public function fetch_conversation($channel, $mid, $force = false) { // Use Zotfinger to create a signed request @@ -1896,6 +1952,7 @@ class Libzot { dbesc($a['signature']['signer']) ); + foreach ($items as $activity) { $AS = new ActivityStreams($activity); @@ -1915,16 +1972,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'); @@ -1940,9 +1999,8 @@ class Libzot { } } - if ($r) { - $arr['author_xchan'] = $r[0]['hubloc_hash']; + $arr['author_xchan'] = $r['hubloc_hash']; } if ($signer) { @@ -1956,9 +2014,9 @@ class Libzot { $arr['item_verified'] = true; } - if ($AS->data['signed_data']) { - IConfig::Set($arr, 'activitypub', 'signed_data', $AS->data['signed_data'], false); - $j = json_decode($AS->data['signed_data'], true); + if ($AS->meta['signed_data']) { + IConfig::Set($arr, 'activitypub', 'signed_data', $AS->meta['signed_data'], false); + $j = json_decode($AS->meta['signed_data'], true); if ($j) { IConfig::Set($arr, 'activitypub', 'rawmsg', json_encode(JSalmon::unpack($j['data'])), true); } @@ -1967,7 +2025,7 @@ class Libzot { logger('FOF Activity received: ' . print_r($arr, true), LOGGER_DATA, LOG_DEBUG); logger('FOF Activity recipient: ' . $channel['channel_hash'], LOGGER_DATA, LOG_DEBUG); - $result = self::process_delivery($arr['owner_xchan'], $AS, $arr, [$channel['channel_hash']], false, false, true); + $result = self::process_delivery($arr['owner_xchan'], $AS, $arr, [$channel['channel_hash']], false, false, true, $force); if ($result) { $ret = array_merge($ret, $result); } @@ -2210,90 +2268,6 @@ class Libzot { return $post_id; } - static function process_mail_delivery($sender, $arr, $deliveries) { - - $result = []; - - if ($sender != $arr['from_xchan']) { - logger('process_mail_delivery: sender is not mail author'); - return; - } - - foreach ($deliveries as $d) { - - $DR = new DReport(z_root(), $sender, $d, $arr['mid']); - - $r = q("select * from channel where channel_hash = '%s' limit 1", - dbesc($d['hash']) - ); - - if (!$r) { - $DR->update('recipient not found'); - $result[] = $DR->get(); - continue; - } - - $channel = $r[0]; - $DR->set_name($channel['channel_name'] . ' <' . channel_reddress($channel) . '>'); - - - if (!perm_is_allowed($channel['channel_id'], $sender, 'post_mail')) { - - /* - * Always allow somebody to reply if you initiated the conversation. It's anti-social - * and a bit rude to send a private message to somebody and block their ability to respond. - * If you are being harrassed and want to put an end to it, delete the conversation. - */ - - $return = false; - if ($arr['parent_mid']) { - $return = q("select * from mail where mid = '%s' and channel_id = %d limit 1", - dbesc($arr['parent_mid']), - intval($channel['channel_id']) - ); - } - if (!$return) { - logger("permission denied for mail delivery {$channel['channel_id']}"); - $DR->update('permission denied'); - $result[] = $DR->get(); - continue; - } - } - - - $r = q("select id from mail where mid = '%s' and channel_id = %d limit 1", - dbesc($arr['mid']), - intval($channel['channel_id']) - ); - if ($r) { - if (intval($arr['mail_recalled'])) { - $x = q("delete from mail where id = %d and channel_id = %d", - intval($r[0]['id']), - intval($channel['channel_id']) - ); - $DR->update('mail recalled'); - $result[] = $DR->get(); - logger('mail_recalled'); - } - else { - $DR->update('duplicate mail received'); - $result[] = $DR->get(); - logger('duplicate mail received'); - } - continue; - } - else { - $arr['account_id'] = $channel['channel_account_id']; - $arr['channel_id'] = $channel['channel_id']; - $item_id = mail_store($arr); - $DR->update('mail delivered'); - $result[] = $DR->get(); - } - } - - return $result; - } - /** * @brief Processes delivery of profile. @@ -2534,14 +2508,14 @@ class Libzot { $access_policy = ACCESS_PRIVATE; } - $directory_url = htmlspecialchars($arr['directory_url'], ENT_COMPAT, 'UTF-8', false); - $url = htmlspecialchars(strtolower($arr['url']), ENT_COMPAT, 'UTF-8', false); - $sellpage = htmlspecialchars($arr['sellpage'], ENT_COMPAT, 'UTF-8', false); - $site_location = htmlspecialchars($arr['location'], ENT_COMPAT, 'UTF-8', false); - $site_realm = htmlspecialchars($arr['realm'], ENT_COMPAT, 'UTF-8', false); - $site_project = htmlspecialchars($arr['project'], ENT_COMPAT, 'UTF-8', false); - $site_crypto = ((array_key_exists('encryption', $arr) && is_array($arr['encryption'])) ? htmlspecialchars(implode(',', $arr['encryption']), ENT_COMPAT, 'UTF-8', false) : ''); - $site_version = ((array_key_exists('version', $arr)) ? htmlspecialchars($arr['version'], ENT_COMPAT, 'UTF-8', false) : ''); + $directory_url = htmlspecialchars((string)$arr['directory_url'], ENT_COMPAT, 'UTF-8', false); + $url = htmlspecialchars((string)strtolower($arr['url']), ENT_COMPAT, 'UTF-8', false); + $sellpage = htmlspecialchars((string)$arr['sellpage'], ENT_COMPAT, 'UTF-8', false); + $site_location = htmlspecialchars((string)$arr['location'], ENT_COMPAT, 'UTF-8', false); + $site_realm = htmlspecialchars((string)$arr['realm'], ENT_COMPAT, 'UTF-8', false); + $site_project = htmlspecialchars((string)$arr['project'], ENT_COMPAT, 'UTF-8', false); + $site_crypto = ((array_key_exists('encryption', $arr) && is_array($arr['encryption'])) ? htmlspecialchars((string)implode(',', $arr['encryption']), ENT_COMPAT, 'UTF-8', false) : ''); + $site_version = ((array_key_exists('version', $arr)) ? htmlspecialchars((string)$arr['version'], ENT_COMPAT, 'UTF-8', false) : ''); // You can have one and only one primary directory per realm. // Downgrade any others claiming to be primary. As they have @@ -2664,29 +2638,34 @@ class Libzot { // we may only end up with one; which results in posts with no author name or photo and are a bit // of a hassle to repair. If either or both are missing, do a full discovery probe. - //if (!array_key_exists('id', $x)) { - //return import_author_activitypub($x); - //} + if(!isset($x['id']) && !isset($x['key']) && !isset($x['id_sig'])) { + return false; + } $hash = self::make_xchan_hash($x['id'], $x['key']); $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. @@ -2704,16 +2683,17 @@ class Libzot { // cached entry and the identity is valid. It's just unreachable until they bring back their // server from the grave or create another clone elsewhere. - if ($site_dead) { - logger('dead site - ignoring', LOGGER_DEBUG, LOG_INFO); + if ($primary_dead || ! $found_primary) { + logger('dead or unknown primary site - ignoring', LOGGER_DEBUG, LOG_INFO); $r = q("select hubloc_id_url from hubloc left join site on hubloc_url = site_url where hubloc_hash = '%s' and site_dead = 0", dbesc($hash) ); + if ($r) { - logger('found another site that is not dead: ' . $r[0]['hubloc_url'], LOGGER_DEBUG, LOG_INFO); - $desturl = $r[0]['hubloc_url']; + logger('found another site that is not dead: ' . $r[0]['hubloc_id_url'], LOGGER_DEBUG, LOG_INFO); + $desturl = $r[0]['hubloc_id_url']; } else { return $hash; @@ -2821,7 +2801,6 @@ class Libzot { } $e = $r[0]; - $id = $e['channel_id']; $sys_channel = (intval($e['channel_system']) ? true : false); @@ -2834,28 +2813,6 @@ class Libzot { if ($deleted || $censored || $sys_channel) $searchable = false; - $public_forum = false; - - $role = get_pconfig($e['channel_id'], 'system', 'permissions_role'); - if ($role === 'forum' || $role === 'repository') { - $public_forum = true; - } - else { - // check if it has characteristics of a public forum based on custom permissions. - $m = Permissions::FilledAutoperms($e['channel_id']); - if ($m) { - foreach ($m as $k => $v) { - if ($k == 'tag_deliver' && intval($v) == 1) - $ch++; - if ($k == 'send_stream' && intval($v) == 0) - $ch++; - } - if ($ch == 2) - $public_forum = true; - } - } - - // This is for birthdays and keywords, but must check access permissions $p = q("select * from profile where uid = %d and is_default = 1", intval($e['channel_id']) @@ -2914,6 +2871,7 @@ class Libzot { ]; $ret['public_key'] = $e['xchan_pubkey']; + $ret['signing_algorithm'] = 'rsa-sha256'; $ret['username'] = $e['channel_address']; $ret['name'] = $e['xchan_name']; $ret['name_updated'] = $e['xchan_name_date']; @@ -2924,10 +2882,11 @@ class Libzot { ]; $ret['channel_role'] = get_pconfig($e['channel_id'], 'system', 'permissions_role', 'custom'); + $ret['channel_type'] = ((get_pconfig($e['channel_id'], 'system', 'group_actor')) ? 'group' : 'normal'); $hookinfo = [ 'channel_id' => $id, - 'protocols' => ['zot6', 'zot'] + 'protocols' => ['zot6'] ]; /** * @hooks channel_protocols @@ -2939,16 +2898,19 @@ class Libzot { $ret['protocols'] = $hookinfo['protocols']; $ret['searchable'] = $searchable; $ret['adult_content'] = $adult_channel; - $ret['public_forum'] = $public_forum; + // now all forums (public, restricted, and private) set the public_forum flag. So it really means "is a group" + // and has nothing to do with accessibility. + $ret['public_forum'] = get_pconfig($e['channel_id'], 'system', 'group_actor'); $ret['comments'] = map_scope(PermissionLimits::Get($e['channel_id'], 'post_comments')); $ret['mail'] = map_scope(PermissionLimits::Get($e['channel_id'], 'post_mail')); if ($deleted) $ret['deleted'] = $deleted; - if (intval($e['channel_removed'])) + if (intval($e['channel_removed'])) { $ret['deleted_locally'] = true; + } // premium or other channel desiring some contact with potential followers before connecting. // This is a template - %s will be replaced with the follow_url we discover for the return channel. @@ -3231,14 +3193,15 @@ class Libzot { return $v; } } - foreach ($arr as $v) { - if ($v[$check] === 'zot') { - return $v; - } - } return $arr[0]; } + static function update_cached_hubloc($hubloc) { + if ($hubloc['hubloc_updated'] > datetime_convert('UTC','UTC','now - 3 days') || $hubloc['hubloc_url'] === z_root()) { + return; + } + self::refresh( [ 'hubloc_id_url' => $hubloc['hubloc_id_url'] ] ); + } } diff --git a/Zotlabs/Lib/Libzotdir.php b/Zotlabs/Lib/Libzotdir.php index 41c0a54e9..4f35a1b80 100644 --- a/Zotlabs/Lib/Libzotdir.php +++ b/Zotlabs/Lib/Libzotdir.php @@ -207,8 +207,6 @@ class Libzotdir { ); } - - // If there are no directory servers, setup the fallback master /** @FIXME What to do if we're in a different realm? */ @@ -249,11 +247,12 @@ class Libzotdir { $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 : '')); + if (! $x['success']) continue; $j = json_decode($x['body'],true); - if (!($j['transactions']) || ($j['ratings'])) + if (!$j['transactions']) continue; q("update site set site_sync = '%s' where site_url = '%s'", @@ -265,6 +264,11 @@ class Libzotdir { if (is_array($j['transactions']) && count($j['transactions'])) { foreach ($j['transactions'] as $t) { + + if (empty($t['hash']) || empty($t['transaction_id']) || empty($t['address'])) { + continue; + } + $r = q("select * from updates where ud_guid = '%s' limit 1", dbesc($t['transaction_id']) ); @@ -319,6 +323,14 @@ class Libzotdir { } if(array_path_exists('signature/signer',$zf) && $zf['signature']['signer'] === $href && intval($zf['signature']['header_valid'])) { $xc = Libzot::import_xchan($zf['data'], 0, $ud); + // This is a workaround for a missing xchan_updated column + // TODO: implement xchan_updated in the xchan table and update this column instead + if($zf['data']['primary_location']['address'] && $zf['data']['primary_location']['url']) { + q("UPDATE hubloc SET hubloc_updated = '%s' WHERE hubloc_id_url = '%s' AND hubloc_primary = 1", + dbesc(datetime_convert()), + dbesc($zf['data']['primary_location']['url']) + ); + } } else { q("update updates set ud_last = '%s' where ud_addr = '%s'", @@ -345,7 +357,7 @@ class Libzotdir { logger('local_dir_update: uid: ' . $uid, LOGGER_DEBUG); - $p = q("select channel.channel_hash, channel_address, channel_timezone, channel_portable_id, profile.* from profile left join channel on channel_id = uid where uid = %d and is_default = 1", + $p = q("select channel.channel_hash, channel_address, channel_timezone, profile.* from profile left join channel on channel_id = uid where uid = %d and is_default = 1", intval($uid) ); @@ -354,7 +366,6 @@ class Libzotdir { if ($p) { $hash = $p[0]['channel_hash']; - $legacy_hash = $p[0]['channel_portable_id']; $profile['description'] = $p[0]['pdesc']; $profile['birthday'] = $p[0]['dob']; @@ -393,10 +404,9 @@ class Libzotdir { ); if(intval($r[0]['xchan_hidden']) != $hidden) { - $r = q("update xchan set xchan_hidden = %d where xchan_hash in ('%s', '%s')", + $r = q("update xchan set xchan_hidden = %d where xchan_hash = '%s'", intval($hidden), - dbesc($hash), - dbesc($legacy_hash) + dbesc($hash) ); } @@ -410,13 +420,11 @@ class Libzotdir { } else { // they may have made it private - q("delete from xprof where xprof_hash in ('%s', '%s')", - dbesc($hash), - dbesc($legacy_hash) + q("delete from xprof where xprof_hash = '%s'", + dbesc($hash) ); - q("delete from xtag where xtag_hash in ('%s', '%s')", - dbesc($hash), - dbesc($legacy_hash) + q("delete from xtag where xtag_hash = '%s'", + dbesc($hash) ); } @@ -635,8 +643,13 @@ class Libzotdir { $dirmode = intval(get_config('system', 'directory_mode')); - if($dirmode == DIRECTORY_MODE_NORMAL) + if($dirmode == DIRECTORY_MODE_NORMAL) { return; + } + + if (empty($hash) || empty($guid) || empty($addr)) { + return; + } if($flags) { q("insert into updates (ud_hash, ud_guid, ud_date, ud_flags, ud_addr ) values ( '%s', '%s', '%s', %d, '%s' )", @@ -656,9 +669,4 @@ class Libzotdir { } } - - - - - } diff --git a/Zotlabs/Lib/MessageFilter.php b/Zotlabs/Lib/MessageFilter.php index 750d6d424..95721e7c7 100644 --- a/Zotlabs/Lib/MessageFilter.php +++ b/Zotlabs/Lib/MessageFilter.php @@ -2,87 +2,104 @@ namespace Zotlabs\Lib; - +require_once('include/html2plain.php'); class MessageFilter { + public static function evaluate($item, $incl, $excl) { - static public function evaluate($item,$incl,$excl) { - - require_once('include/html2plain.php'); - - unobscure($item); - - $text = prepare_text($item['body'],$item['mimetype']); + $text = prepare_text($item['body'],((isset($item['mimetype'])) ? $item['mimetype'] : 'text/x-multicode')); $text = html2plain(($item['title']) ? $item['title'] . ' ' . $text : $text); - $lang = null; - if((strpos($incl,'lang=') !== false) || (strpos($excl,'lang=') !== false) || (strpos($incl,'lang!=') !== false) || (strpos($excl,'lang!=') !== false)) { + if ((strpos($incl, 'lang=') !== false) || (strpos($excl, 'lang=') !== false) || (strpos($incl, 'lang!=') !== false) || (strpos($excl, 'lang!=') !== false)) { $lang = detect_language($text); } - $tags = ((is_array($item['term']) && count($item['term'])) ? $item['term'] : false); + $tags = ((isset($item['term']) && is_array($item['term']) && count($item['term'])) ? $item['term'] : false); // exclude always has priority - $exclude = (($excl) ? explode("\n",$excl) : null); + $exclude = (($excl) ? explode("\n", $excl) : null); - if($exclude) { - foreach($exclude as $word) { + if ($exclude) { + foreach ($exclude as $word) { $word = trim($word); - if(! $word) + if (! $word) { continue; - if(substr($word,0,1) === '#' && $tags) { - foreach($tags as $t) - if((($t['ttype'] == TERM_HASHTAG) || ($t['ttype'] == TERM_COMMUNITYTAG)) && (($t['term'] === substr($word,1)) || (substr($word,1) === '*'))) - return false; } - elseif(substr($word,0,1) === '$' && $tags) { - foreach($tags as $t) - if(($t['ttype'] == TERM_CATEGORY) && (($t['term'] === substr($word,1)) || (substr($word,1) === '*'))) + if (substr($word, 0, 1) === '#' && $tags) { + foreach ($tags as $t) { + if ((($t['ttype'] == TERM_HASHTAG) || ($t['ttype'] == TERM_COMMUNITYTAG)) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) { return false; - } - elseif((strpos($word,'/') === 0) && preg_match($word,$text)) + } + } + } elseif (substr($word, 0, 1) === '$' && $tags) { + foreach ($tags as $t) { + if (($t['ttype'] == TERM_CATEGORY) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) { + return false; + } + } + } elseif (substr($word, 0, 2) === '?+') { + if (self::test_condition(substr($word, 2), $item['obj'])) { + return false; + } + } elseif (substr($word, 0, 1) === '?') { + if (self::test_condition(substr($word, 1), $item)) { + return false; + } + } elseif ((strpos($word, '/') === 0) && preg_match($word, $text)) { return false; - elseif((strpos($word,'lang=') === 0) && ($lang) && (strcasecmp($lang,trim(substr($word,5))) == 0)) + } elseif ((strpos($word, 'lang=') === 0) && ($lang) && (strcasecmp($lang, trim(substr($word, 5))) == 0)) { return false; - elseif((strpos($word,'lang!=') === 0) && ($lang) && (strcasecmp($lang,trim(substr($word,6))) != 0)) + } elseif ((strpos($word, 'lang!=') === 0) && ($lang) && (strcasecmp($lang, trim(substr($word, 6))) != 0)) { return false; - elseif(stristr($text,$word) !== false) + } elseif (stristr($text, $word) !== false) { return false; + } } } - $include = (($incl) ? explode("\n",$incl) : null); + $include = (($incl) ? explode("\n", $incl) : null); - if($include) { - foreach($include as $word) { + if ($include) { + foreach ($include as $word) { $word = trim($word); - if(! $word) + if (! $word) { continue; - if(substr($word,0,1) === '#' && $tags) { - foreach($tags as $t) - if((($t['ttype'] == TERM_HASHTAG) || ($t['ttype'] == TERM_COMMUNITYTAG)) && (($t['term'] === substr($word,1)) || (substr($word,1) === '*'))) - return true; } - elseif(substr($word,0,1) === '$' && $tags) { - foreach($tags as $t) - if(($t['ttype'] == TERM_CATEGORY) && (($t['term'] === substr($word,1)) || (substr($word,1) === '*'))) + if (substr($word, 0, 1) === '#' && $tags) { + foreach ($tags as $t) { + if ((($t['ttype'] == TERM_HASHTAG) || ($t['ttype'] == TERM_COMMUNITYTAG)) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) { return true; - } - elseif((strpos($word,'/') === 0) && preg_match($word,$text)) + } + } + } elseif (substr($word, 0, 1) === '$' && $tags) { + foreach ($tags as $t) { + if (($t['ttype'] == TERM_CATEGORY) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) { + return true; + } + } + } elseif (substr($word, 0, 2) === '?+') { + if (self::test_condition(substr($word, 2), $item['obj'])) { + return true; + } + } elseif (substr($word, 0, 1) === '?') { + if (self::test_condition(substr($word, 1), $item)) { + return true; + } + } elseif ((strpos($word, '/') === 0) && preg_match($word, $text)) { return true; - elseif((strpos($word,'lang=') === 0) && ($lang) && (strcasecmp($lang,trim(substr($word,5))) == 0)) + } elseif ((strpos($word, 'lang=') === 0) && ($lang) && (strcasecmp($lang, trim(substr($word, 5))) == 0)) { return true; - elseif((strpos($word,'lang!=') === 0) && ($lang) && (strcasecmp($lang,trim(substr($word,6))) != 0)) + } elseif ((strpos($word, 'lang!=') === 0) && ($lang) && (strcasecmp($lang, trim(substr($word, 6))) != 0)) { return true; - elseif(stristr($text,$word) !== false) + } elseif (stristr($text, $word) !== false) { return true; + } } - } - else { + } else { return true; } @@ -90,4 +107,113 @@ class MessageFilter { } + /** + * @brief Test for Conditional Execution conditions. Shamelessly ripped off from Code/Render/Comanche + * + * This is extensible. The first version of variable testing supports tests of the forms: + * + * - ?foo ~= baz which will check if item.foo contains the string 'baz'; + * - ?foo == baz which will check if item.foo is the string 'baz'; + * - ?foo != baz which will check if item.foo is not the string 'baz'; + * - ?foo >= 3 which will check if item.foo is greater than or equal to 3; + * - ?foo > 3 which will check if item.foo is greater than 3; + * - ?foo <= 3 which will check if item.foo is less than or equal to 3; + * - ?foo < 3 which will check if item.foo is less than 3; + * + * - ?foo {} baz which will check if 'baz' is an array element in item.foo + * - ?foo {*} baz which will check if 'baz' is an array key in item.foo + * - ?foo which will check for a return of a true condition for item.foo; + * + * The values 0, '', an empty array, and an unset value will all evaluate to false. + * + * @param string $s + * @param array $item + * @return bool + */ + + public static function test_condition($s,$item) { + + if (preg_match('/(.*?)\s\~\=\s(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); + if (stripos($x, trim($matches[2])) !== false) { + return true; + } + return false; + } + + if (preg_match('/(.*?)\s\=\=\s(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); + if ($x == trim($matches[2])) { + return true; + } + return false; + } + + if (preg_match('/(.*?)\s\!\=\s(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); + if ($x != trim($matches[2])) { + return true; + } + return false; + } + + if (preg_match('/(.*?)\s\>\=\s(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); + if ($x >= trim($matches[2])) { + return true; + } + return false; + } + + if (preg_match('/(.*?)\s\<\=\s(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); + if ($x <= trim($matches[2])) { + return true; + } + return false; + } + + if (preg_match('/(.*?)\s\>\s(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); + if ($x > trim($matches[2])) { + return true; + } + return false; + } + + if (preg_match('/(.*?)\s\>\s(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); + if ($x < trim($matches[2])) { + return true; + } + return false; + } + + if (preg_match('/[\$](.*?)\s\{\}\s(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); + if (is_array($x) && in_array(trim($matches[2]), $x)) { + return true; + } + return false; + } + + if (preg_match('/(.*?)\s\{\*\}\s(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); + if (is_array($x) && array_key_exists(trim($matches[2]), $x)) { + return true; + } + return false; + } + + if (preg_match('/(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); + if ($x) { + return true; + } + return false; + } + + return false; + } + } diff --git a/Zotlabs/Lib/NativeWiki.php b/Zotlabs/Lib/NativeWiki.php index 9e6a3ac85..bf4ac8e87 100644 --- a/Zotlabs/Lib/NativeWiki.php +++ b/Zotlabs/Lib/NativeWiki.php @@ -12,8 +12,8 @@ class NativeWiki { public static function listwikis($channel, $observer_hash) { $sql_extra = item_permissions_sql($channel['channel_id'], $observer_hash); - $wikis = q("SELECT * FROM item - WHERE resource_type = '%s' AND mid = parent_mid AND uid = %d AND item_deleted = 0 $sql_extra", + $wikis = q("SELECT * FROM item + WHERE resource_type = '%s' AND mid = parent_mid AND uid = %d AND item_deleted = 0 $sql_extra", dbesc(NWIKI_ITEM_RESOURCE_TYPE), intval($channel['channel_id']) ); @@ -49,7 +49,7 @@ class NativeWiki { $mid = z_root() . '/item/' . $uuid; $arr = array(); // Initialize the array of parameters for the post - $item_hidden = ((intval($wiki['postVisible']) === 0) ? 1 : 0); + $item_hidden = ((intval($wiki['postVisible']) === 0) ? 1 : 0); $wiki_url = z_root() . '/wiki/' . $channel['channel_address'] . '/' . $wiki['urlName']; $arr['aid'] = $channel['channel_account_id']; $arr['uuid'] = $uuid; @@ -61,8 +61,8 @@ class NativeWiki { $arr['resource_id'] = $resource_id; $arr['owner_xchan'] = $channel['channel_hash']; $arr['author_xchan'] = $observer_hash; - $arr['plink'] = z_root() . '/channel/' . $channel['channel_address'] . '/?f=&mid=' . urlencode($arr['mid']); - $arr['llink'] = $arr['plink']; + $arr['plink'] = $mid; + $arr['llink'] = z_root() . '/display/' . gen_link_id($mid); $arr['title'] = $wiki['htmlName']; // name of new wiki; $arr['allow_cid'] = $ac['allow_cid']; $arr['allow_gid'] = $ac['allow_gid']; @@ -133,13 +133,13 @@ class NativeWiki { // update acl for any existing wiki pages q("update item set allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s', item_private = %d where resource_type = 'nwikipage' and resource_id = '%s'", - dbesc($item['allow_cid']), - dbesc($item['allow_gid']), - dbesc($item['deny_cid']), - dbesc($item['deny_gid']), - dbesc($item['item_private']), + dbesc($item['allow_cid']), + dbesc($item['allow_gid']), + dbesc($item['deny_cid']), + dbesc($item['deny_gid']), + dbesc($item['item_private']), dbesc($arr['resource_id']) - ); + ); if($update['item_id']) { @@ -211,12 +211,12 @@ class NativeWiki { public static function get_wiki($channel_id, $observer_hash, $resource_id) { - + $sql_extra = item_permissions_sql($channel_id,$observer_hash); - $item = q("SELECT * FROM item WHERE uid = %d AND resource_type = '%s' AND resource_id = '%s' AND item_deleted = 0 + $item = q("SELECT * FROM item WHERE uid = %d AND resource_type = '%s' AND resource_id = '%s' AND item_deleted = 0 $sql_extra ORDER BY id LIMIT 1", - intval($channel_id), + intval($channel_id), dbesc(NWIKI_ITEM_RESOURCE_TYPE), dbesc($resource_id) ); @@ -224,7 +224,7 @@ class NativeWiki { return [ 'wiki' => null ]; } else { - + $w = $item[0]; // wiki item table record // Get wiki metadata $rawName = get_iconfig($w, 'wiki', 'rawName'); @@ -246,20 +246,20 @@ class NativeWiki { public static function exists_by_name($uid, $urlName) { - $sql_extra = item_permissions_sql($uid); + $sql_extra = item_permissions_sql($uid); - $item = q("SELECT item.id, resource_id FROM item left join iconfig on iconfig.iid = item.id - WHERE resource_type = '%s' AND iconfig.v = '%s' AND uid = %d - AND item_deleted = 0 $sql_extra limit 1", - dbesc(NWIKI_ITEM_RESOURCE_TYPE), - //dbesc(urldecode($urlName)), + $item = q("SELECT item.id, resource_id FROM item left join iconfig on iconfig.iid = item.id + WHERE resource_type = '%s' AND iconfig.v = '%s' AND uid = %d + AND item_deleted = 0 $sql_extra limit 1", + dbesc(NWIKI_ITEM_RESOURCE_TYPE), + //dbesc(urldecode($urlName)), dbesc(self::name_decode($urlName)), intval($uid) ); if($item) { return array('id' => $item[0]['id'], 'resource_id' => $item[0]['resource_id']); - } + } else { return array('id' => null, 'resource_id' => null); } @@ -277,7 +277,7 @@ class NativeWiki { $r = q("SELECT * FROM item WHERE uid = %d and resource_type = '%s' AND resource_id = '%s' $sql_extra LIMIT 1", intval($owner_id), - dbesc(NWIKI_ITEM_RESOURCE_TYPE), + dbesc(NWIKI_ITEM_RESOURCE_TYPE), dbesc($resource_id) ); @@ -285,8 +285,6 @@ class NativeWiki { return array('read' => false, 'write' => false, 'success' => true); } else { - // TODO: Create a new permission setting for wiki analogous to webpages. Until - // then, use webpage permissions $write = perm_is_allowed($owner_id, $observer_hash,'write_wiki'); return array('read' => true, 'write' => $write, 'success' => true); } diff --git a/Zotlabs/Lib/NativeWikiPage.php b/Zotlabs/Lib/NativeWikiPage.php index 3c61ea800..1e944f7ac 100644 --- a/Zotlabs/Lib/NativeWikiPage.php +++ b/Zotlabs/Lib/NativeWikiPage.php @@ -2,14 +2,15 @@ namespace Zotlabs\Lib; -use \Zotlabs\Lib as Zlib; +use App; +use Zotlabs\Access\PermissionLimits; class NativeWikiPage { - static public function page_list($channel_id,$observer_hash, $resource_id) { + static public function page_list($channel_id, $observer_hash, $resource_id) { // TODO: Create item table records for pages so that metadata like title can be applied - $w = Zlib\NativeWiki::get_wiki($channel_id,$observer_hash,$resource_id); + $w = NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); $pages[] = [ 'resource_id' => '', @@ -18,134 +19,149 @@ class NativeWikiPage { 'link_id' => 'id_wiki_home_0' ]; - $sql_extra = item_permissions_sql($channel_id,$observer_hash); + $sql_extra = item_permissions_sql($channel_id, $observer_hash); - $r = q("select * from item where resource_type = 'nwikipage' and resource_id = '%s' and uid = %d and item_deleted = 0 + $r = q("select * from item where resource_type = 'nwikipage' and resource_id = '%s' and uid = %d and item_deleted = 0 $sql_extra order by title asc", dbesc($resource_id), intval($channel_id) ); - if($r) { + if ($r) { $x = []; $y = []; - foreach($r as $rv) { - if(! in_array($rv['mid'],$x)) { + foreach ($r as $rv) { + if (!in_array($rv['mid'], $x)) { $y[] = $rv; $x[] = $rv['mid']; } } - $items = fetch_post_tags($y,true); + $items = fetch_post_tags($y, true); - foreach($items as $page_item) { - $title = get_iconfig($page_item['id'],'nwikipage','pagetitle',t('(No Title)')); - if(urldecode($title) !== 'Home') { + foreach ($items as $page_item) { + $title = get_iconfig($page_item['id'], 'nwikipage', 'pagetitle', t('(No Title)')); + if (urldecode($title) !== 'Home') { $pages[] = [ 'resource_id' => $resource_id, 'title' => escape_tags($title), //'url' => str_replace('%2F','/',urlencode(str_replace('%2F','/',urlencode($title)))), - 'url' => Zlib\NativeWiki::name_encode($title), + 'url' => NativeWiki::name_encode($title), 'link_id' => 'id_' . substr($resource_id, 0, 10) . '_' . $page_item['id'] ]; } } } - return array('pages' => $pages, 'wiki' => $w); + return ['pages' => $pages, 'wiki' => $w]; } - static public function create_page($channel_id, $observer_hash, $name, $resource_id, $mimetype = 'text/bbcode') { + static public function create_page($channel, $observer_hash, $name, $resource_id, $mimetype = 'text/bbcode') { logger('mimetype: ' . $mimetype); - if(! in_array($mimetype,[ 'text/markdown','text/bbcode','text/plain','text/html' ])) + if (!in_array($mimetype, ['text/markdown', 'text/bbcode', 'text/plain', 'text/html'])) $mimetype = 'text/markdown'; - $w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); + $w = NativeWiki::get_wiki($channel['channel_id'], $observer_hash, $resource_id); - if (! $w['wiki']) { - return array('content' => null, 'message' => 'Error reading wiki', 'success' => false); + if (!$w['wiki']) { + return ['content' => null, 'message' => 'Error reading wiki', 'success' => false]; } // backslashes won't work well in the javascript functions - $name = str_replace('\\','',$name); - - // create an empty activity + $name = str_replace('\\', '', $name); - $arr = []; - $arr['uid'] = $channel_id; - $arr['author_xchan'] = $observer_hash; - $arr['mimetype'] = $mimetype; - $arr['title'] = $name; - $arr['resource_type'] = 'nwikipage'; - $arr['resource_id'] = $resource_id; - $arr['allow_cid'] = $w['wiki']['allow_cid']; - $arr['allow_gid'] = $w['wiki']['allow_gid']; - $arr['deny_cid'] = $w['wiki']['deny_cid']; - $arr['deny_gid'] = $w['wiki']['deny_gid']; + $uuid = new_uuid(); + $mid = z_root() . '/item/' . $uuid; - $arr['public_policy'] = map_scope(\Zotlabs\Access\PermissionLimits::Get($channel_id,'view_wiki'),true); + // create an empty activity + $arr = []; + $arr['aid'] = $channel['channel_account_id']; + $arr['uid'] = $channel['channel_id']; + $arr['mid'] = $mid; + $arr['parent_mid'] = $w['wiki']['mid']; + $arr['parent'] = $w['wiki']['parent']; + $arr['uuid'] = $uuid; + $arr['item_hidden'] = $w['wiki']['item_hidden']; + $arr['plink'] = $mid; + $arr['llink'] = z_root() . '/display/' . gen_link_id($mid); + $arr['author_xchan'] = $observer_hash; + $arr['mimetype'] = $mimetype; + $arr['title'] = $name; + $arr['resource_type'] = 'nwikipage'; + $arr['resource_id'] = $resource_id; + $arr['allow_cid'] = $w['wiki']['allow_cid']; + $arr['allow_gid'] = $w['wiki']['allow_gid']; + $arr['deny_cid'] = $w['wiki']['deny_cid']; + $arr['deny_gid'] = $w['wiki']['deny_gid']; + $arr['item_private'] = $w['wiki']['item_private']; + $arr['item_wall'] = 1; + $arr['item_origin'] = 1; + $arr['item_thread_top'] = 1; + $arr['verb'] = ACTIVITY_CREATE; + $arr['obj_type'] = 'Document'; + // TODO: add an object? + $arr['public_policy'] = map_scope(PermissionLimits::Get($channel['channel_id'], 'view_wiki'), true); // We may wish to change this some day. $arr['item_unpublished'] = 1; - set_iconfig($arr,'nwikipage','pagetitle',(($name) ? $name : t('(No Title)')),true); - - $p = post_activity_item($arr, false, false); + set_iconfig($arr, 'nwikipage', 'pagetitle', (($name) ? $name : t('(No Title)')), true); + $p = item_store($arr, false, false); - if($p['item_id']) { - $page = [ + if ($p['item_id']) { + $page = [ 'rawName' => $name, 'htmlName' => escape_tags($name), - //'urlName' => urlencode($name), - 'urlName' => Zlib\NativeWiki::name_encode($name) + //'urlName' => urlencode($name), + 'urlName' => NativeWiki::name_encode($name) ]; - return array('page' => $page, 'item_id' => $p['item_id'], 'item' => $p['activity'], 'wiki' => $w, 'message' => '', 'success' => true); + return ['page' => $page, 'item_id' => $p['item_id'], 'item' => $p['activity'], 'wiki' => $w, 'message' => '', 'success' => true]; } - return [ 'success' => false, 'message' => t('Wiki page create failed.') ]; + return ['success' => false, 'message' => t('Wiki page create failed.')]; } static public function rename_page($arr) { - $pageUrlName = ((array_key_exists('pageUrlName',$arr)) ? $arr['pageUrlName'] : ''); - $pageNewName = ((array_key_exists('pageNewName',$arr)) ? $arr['pageNewName'] : ''); - $resource_id = ((array_key_exists('resource_id',$arr)) ? $arr['resource_id'] : ''); - $observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : ''); - $channel_id = ((array_key_exists('channel_id',$arr)) ? $arr['channel_id'] : 0); + $pageUrlName = ((array_key_exists('pageUrlName', $arr)) ? $arr['pageUrlName'] : ''); + $pageNewName = ((array_key_exists('pageNewName', $arr)) ? $arr['pageNewName'] : ''); + $resource_id = ((array_key_exists('resource_id', $arr)) ? $arr['resource_id'] : ''); + $observer_hash = ((array_key_exists('observer_hash', $arr)) ? $arr['observer_hash'] : ''); + $channel_id = ((array_key_exists('channel_id', $arr)) ? $arr['channel_id'] : 0); - $w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); - if(! $w['wiki']) { - return array('message' => t('Wiki not found.'), 'success' => false); + $w = NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); + if (!$w['wiki']) { + return ['message' => t('Wiki not found.'), 'success' => false]; } - $ic = q("select * from iconfig left join item on iconfig.iid = item.id + $ic = q("select * from iconfig left join item on iconfig.iid = item.id where uid = %d and cat = 'nwikipage' and k = 'pagetitle' and v = '%s'", intval($channel_id), dbesc($pageNewName) ); - if($ic) { - return [ 'success' => false, 'message' => t('Destination name already exists') ]; + if ($ic) { + return ['success' => false, 'message' => t('Destination name already exists')]; } $ids = []; - $ic = q("select *, item.id as item_id from iconfig left join item on iconfig.iid = item.id + $ic = q("select *, item.id as item_id from iconfig left join item on iconfig.iid = item.id where uid = %d and cat = 'nwikipage' and k = 'pagetitle' and v = '%s'", intval($channel_id), dbesc($pageUrlName) ); - if($ic) { - foreach($ic as $c) { - set_iconfig($c['item_id'],'nwikipage','pagetitle',$pageNewName); + if ($ic) { + foreach ($ic as $c) { + set_iconfig($c['item_id'], 'nwikipage', 'pagetitle', $pageNewName); $ids[] = $c['item_id']; } @@ -154,105 +170,101 @@ class NativeWikiPage { dbesc($pageNewName) ); - $page = [ - 'rawName' => $pageNewName, - 'htmlName' => escape_tags($pageNewName), + $page = [ + 'rawName' => $pageNewName, + 'htmlName' => escape_tags($pageNewName), //'urlName' => urlencode(escape_tags($pageNewName)) - 'urlName' => Zlib\NativeWiki::name_encode($pageNewName) + 'urlName' => NativeWiki::name_encode($pageNewName) ]; - return [ 'success' => true, 'page' => $page ]; + return ['success' => true, 'page' => $page]; } - return [ 'success' => false, 'message' => t('Page not found') ]; - + return ['success' => false, 'message' => t('Page not found')]; + } static public function get_page_content($arr) { - $pageUrlName = ((array_key_exists('pageUrlName',$arr)) ? $arr['pageUrlName'] : ''); - $resource_id = ((array_key_exists('resource_id',$arr)) ? $arr['resource_id'] : ''); - $observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : ''); - $channel_id = ((array_key_exists('channel_id',$arr)) ? intval($arr['channel_id']) : 0); - $revision = ((array_key_exists('revision',$arr)) ? intval($arr['revision']) : (-1)); - + $resource_id = ((array_key_exists('resource_id', $arr)) ? $arr['resource_id'] : ''); + $observer_hash = ((array_key_exists('observer_hash', $arr)) ? $arr['observer_hash'] : ''); + $channel_id = ((array_key_exists('channel_id', $arr)) ? intval($arr['channel_id']) : 0); - $w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); - if (! $w['wiki']) { - return array('content' => null, 'message' => 'Error reading wiki', 'success' => false); + $w = NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); + if (!$w['wiki']) { + return ['content' => null, 'message' => 'Error reading wiki', 'success' => false]; } $item = self::load_page($arr); - if($item) { + if ($item) { $content = $item['body']; - return [ + return [ 'content' => $content, 'mimeType' => $w['mimeType'], - 'pageMimeType' => $item['mimetype'], - 'message' => '', + 'pageMimeType' => $item['mimetype'], + 'message' => '', 'success' => true ]; } - - return array('content' => null, 'message' => t('Error reading page content'), 'success' => false); + + return ['content' => null, 'message' => t('Error reading page content'), 'success' => false]; } static public function page_history($arr) { - $pageUrlName = ((array_key_exists('pageUrlName',$arr)) ? $arr['pageUrlName'] : ''); - $resource_id = ((array_key_exists('resource_id',$arr)) ? $arr['resource_id'] : ''); - $observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : ''); - $channel_id = ((array_key_exists('channel_id',$arr)) ? $arr['channel_id'] : 0); + $resource_id = ((array_key_exists('resource_id', $arr)) ? $arr['resource_id'] : ''); + $observer_hash = ((array_key_exists('observer_hash', $arr)) ? $arr['observer_hash'] : ''); + $channel_id = ((array_key_exists('channel_id', $arr)) ? $arr['channel_id'] : 0); - $w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); + $w = NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); if (!$w['wiki']) { - return array('history' => null, 'message' => 'Error reading wiki', 'success' => false); + return ['history' => null, 'message' => 'Error reading wiki', 'success' => false]; } $items = self::load_page_history($arr); $history = []; - if($items) { + if ($items) { $processed = 0; - foreach($items as $item) { - if($processed > 1000) + foreach ($items as $item) { + if ($processed > 1000) break; - $processed ++; - $history[] = [ + $processed++; + $history[] = [ 'revision' => $item['revision'], - 'date' => datetime_convert('UTC',date_default_timezone_get(),$item['edited']), - 'name' => $item['author']['xchan_name'], - 'title' => get_iconfig($item,'nwikipage','commit_msg') + 'date' => datetime_convert('UTC', date_default_timezone_get(), $item['edited']), + 'name' => $item['author']['xchan_name'], + 'title' => get_iconfig($item, 'nwikipage', 'commit_msg') ]; } - return [ 'success' => true, 'history' => $history ]; + return ['success' => true, 'history' => $history]; } - return [ 'success' => false ]; + return ['success' => false]; } - + static public function load_page($arr) { - $pageUrlName = ((array_key_exists('pageUrlName',$arr)) ? $arr['pageUrlName'] : ''); - $resource_id = ((array_key_exists('resource_id',$arr)) ? $arr['resource_id'] : ''); - $observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : ''); - $channel_id = ((array_key_exists('channel_id',$arr)) ? $arr['channel_id'] : 0); - $revision = ((array_key_exists('revision',$arr)) ? $arr['revision'] : (-1)); + $pageUrlName = ((array_key_exists('pageUrlName', $arr)) ? $arr['pageUrlName'] : ''); + $resource_id = ((array_key_exists('resource_id', $arr)) ? $arr['resource_id'] : ''); + $observer_hash = ((array_key_exists('observer_hash', $arr)) ? $arr['observer_hash'] : ''); + $channel_id = ((array_key_exists('channel_id', $arr)) ? $arr['channel_id'] : 0); + $revision = ((array_key_exists('revision', $arr)) ? $arr['revision'] : (-1)); - $w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); + $w = NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); - if (! $w['wiki']) { - return array('content' => null, 'message' => 'Error reading wiki', 'success' => false); + if (!$w['wiki']) { + return ['content' => null, 'message' => 'Error reading wiki', 'success' => false]; } $ids = ''; @@ -262,32 +274,32 @@ class NativeWikiPage { dbesc($pageUrlName) ); - if($ic) { - foreach($ic as $c) { - if($ids) + if ($ic) { + foreach ($ic as $c) { + if ($ids) $ids .= ','; $ids .= intval($c['iid']); } } - $sql_extra = item_permissions_sql($channel_id,$observer_hash); + $sql_extra = item_permissions_sql($channel_id, $observer_hash); - if($revision == (-1)) + if ($revision == (-1)) $sql_extra .= " order by revision desc "; - elseif($revision) + elseif ($revision) $sql_extra .= " and revision = " . intval($revision) . " "; $r = null; - if($ids) { + if ($ids) { $r = q("select * from item where resource_type = 'nwikipage' and resource_id = '%s' and uid = %d and id in ( $ids ) $sql_extra limit 1", dbesc($resource_id), intval($channel_id) ); - if($r) { - $items = fetch_post_tags($r,true); + if ($r) { + $items = fetch_post_tags($r, true); return $items[0]; } } @@ -298,15 +310,14 @@ class NativeWikiPage { static public function load_page_history($arr) { - $pageUrlName = ((array_key_exists('pageUrlName',$arr)) ? $arr['pageUrlName'] : ''); - $resource_id = ((array_key_exists('resource_id',$arr)) ? $arr['resource_id'] : ''); - $observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : ''); - $channel_id = ((array_key_exists('channel_id',$arr)) ? $arr['channel_id'] : 0); - $revision = ((array_key_exists('revision',$arr)) ? $arr['revision'] : (-1)); + $pageUrlName = ((array_key_exists('pageUrlName', $arr)) ? $arr['pageUrlName'] : ''); + $resource_id = ((array_key_exists('resource_id', $arr)) ? $arr['resource_id'] : ''); + $observer_hash = ((array_key_exists('observer_hash', $arr)) ? $arr['observer_hash'] : ''); + $channel_id = ((array_key_exists('channel_id', $arr)) ? $arr['channel_id'] : 0); - $w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); - if (! $w['wiki']) { - return array('content' => null, 'message' => 'Error reading wiki', 'success' => false); + $w = NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); + if (!$w['wiki']) { + return ['content' => null, 'message' => 'Error reading wiki', 'success' => false]; } $ids = ''; @@ -315,28 +326,28 @@ class NativeWikiPage { intval($channel_id), dbesc($pageUrlName) ); - - if($ic) { - foreach($ic as $c) { - if($ids) + + if ($ic) { + foreach ($ic as $c) { + if ($ids) $ids .= ','; $ids .= intval($c['iid']); } } - $sql_extra = item_permissions_sql($channel_id,$observer_hash); + $sql_extra = item_permissions_sql($channel_id, $observer_hash); $sql_extra .= " order by revision desc "; $r = null; - if($ids) { + if ($ids) { $r = q("select * from item where resource_type = 'nwikipage' and resource_id = '%s' and uid = %d and id in ( $ids ) and item_deleted = 0 $sql_extra", dbesc($resource_id), intval($channel_id) ); - if($r) { + if ($r) { xchan_query($r); - $items = fetch_post_tags($r,true); + $items = fetch_post_tags($r, true); return $items; } } @@ -346,31 +357,30 @@ class NativeWikiPage { static public function save_page($arr) { - $pageUrlName = ((array_key_exists('pageUrlName',$arr)) ? $arr['pageUrlName'] : ''); - $content = ((array_key_exists('content',$arr)) ? $arr['content'] : ''); - $resource_id = ((array_key_exists('resource_id',$arr)) ? $arr['resource_id'] : ''); - $observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : ''); - $channel_id = ((array_key_exists('channel_id',$arr)) ? $arr['channel_id'] : 0); - $revision = ((array_key_exists('revision',$arr)) ? $arr['revision'] : 0); + $pageUrlName = ((array_key_exists('pageUrlName', $arr)) ? $arr['pageUrlName'] : ''); + $content = ((array_key_exists('content', $arr)) ? $arr['content'] : ''); + $resource_id = ((array_key_exists('resource_id', $arr)) ? $arr['resource_id'] : ''); + $observer_hash = ((array_key_exists('observer_hash', $arr)) ? $arr['observer_hash'] : ''); + $channel_id = ((array_key_exists('channel_id', $arr)) ? $arr['channel_id'] : 0); - $w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); + $w = NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); if (!$w['wiki']) { - return array('message' => t('Error reading wiki'), 'success' => false); + return ['message' => t('Error reading wiki'), 'success' => false]; } - - // fetch the most recently saved revision. + + // fetch the most recently saved revision. $item = self::load_page($arr); - if(! $item) { - return array('message' => t('Page not found'), 'success' => false); + if (!$item) { + return ['message' => t('Page not found'), 'success' => false]; } $mimetype = $item['mimetype']; - // change just the fields we need to change to create a revision; + // change just the fields we need to change to create a revision; unset($item['id']); unset($item['author']); @@ -381,8 +391,8 @@ class NativeWikiPage { $item['edited'] = datetime_convert(); $item['mimetype'] = $mimetype; - if($item['iconfig'] && is_array($item['iconfig']) && count($item['iconfig'])) { - for($x = 0; $x < count($item['iconfig']); $x ++) { + if ($item['iconfig'] && is_array($item['iconfig']) && count($item['iconfig'])) { + for ($x = 0; $x < count($item['iconfig']); $x++) { unset($item['iconfig'][$x]['id']); unset($item['iconfig'][$x]['iid']); } @@ -390,168 +400,164 @@ class NativeWikiPage { $ret = item_store($item, false, false); - if($ret['item_id']) - return array('message' => '', 'item_id' => $ret['item_id'], 'filename' => $pageUrlName, 'success' => true); + if ($ret['item_id']) + return ['message' => '', 'item_id' => $ret['item_id'], 'filename' => $pageUrlName, 'success' => true]; else - return array('message' => t('Page update failed.'), 'success' => false); - } + return ['message' => t('Page update failed.'), 'success' => false]; + } static public function delete_page($arr) { - $pageUrlName = (array_key_exists('pageUrlName',$arr) ? $arr['pageUrlName'] : ''); - $resource_id = (array_key_exists('resource_id',$arr) ? $arr['resource_id'] : ''); - $observer_hash = (array_key_exists('observer_hash',$arr) ? $arr['observer_hash'] : ''); - $channel_id = (array_key_exists('channel_id',$arr) ? $arr['channel_id'] : 0); + $pageUrlName = (array_key_exists('pageUrlName', $arr) ? $arr['pageUrlName'] : ''); + $resource_id = (array_key_exists('resource_id', $arr) ? $arr['resource_id'] : ''); + $observer_hash = (array_key_exists('observer_hash', $arr) ? $arr['observer_hash'] : ''); + $channel_id = (array_key_exists('channel_id', $arr) ? $arr['channel_id'] : 0); - $w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); - if(! $w['wiki']) { - return [ 'success' => false, 'message' => t('Error reading wiki') ]; + $w = NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); + if (!$w['wiki']) { + return ['success' => false, 'message' => t('Error reading wiki')]; } $ids = []; - $ic = q("select * from iconfig left join item on iconfig.iid = item.id + $ic = q("select * from iconfig left join item on iconfig.iid = item.id where uid = %d and cat = 'nwikipage' and k = 'pagetitle' and v = '%s'", intval($channel_id), dbesc($pageUrlName) ); - if($ic) { - foreach($ic as $c) { + if ($ic) { + foreach ($ic as $c) { $ids[] = intval($c['iid']); } } - if($ids) { + if ($ids) { drop_items($ids, true, DROPITEM_PHASE1); - return [ 'success' => true ]; + return ['success' => true]; } - return [ 'success' => false, 'message' => t('Nothing deleted') ]; + return ['success' => false, 'message' => t('Nothing deleted')]; } - + static public function revert_page($arr) { - $pageUrlName = ((array_key_exists('pageUrlName',$arr)) ? $arr['pageUrlName'] : ''); - $resource_id = ((array_key_exists('resource_id',$arr)) ? $arr['resource_id'] : ''); - $commitHash = ((array_key_exists('commitHash',$arr)) ? $arr['commitHash'] : null); - $observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : ''); - $channel_id = ((array_key_exists('channel_id',$arr)) ? $arr['channel_id'] : 0); + $resource_id = ((array_key_exists('resource_id', $arr)) ? $arr['resource_id'] : ''); + $commitHash = ((array_key_exists('commitHash', $arr)) ? $arr['commitHash'] : null); + $observer_hash = ((array_key_exists('observer_hash', $arr)) ? $arr['observer_hash'] : ''); + $channel_id = ((array_key_exists('channel_id', $arr)) ? $arr['channel_id'] : 0); - if (! $commitHash) { - return array('message' => 'No commit was provided', 'success' => false); + if (!$commitHash) { + return ['message' => 'No commit was provided', 'success' => false]; } - $w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); + $w = NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); if (!$w['wiki']) { - return array('message' => 'Error reading wiki', 'success' => false); + return ['message' => 'Error reading wiki', 'success' => false]; } $x = $arr; - if(intval($commitHash) > 0) { + if (intval($commitHash) > 0) { unset($x['commitHash']); $x['revision'] = intval($commitHash) - 1; - $loaded = self::load_page($x); + $loaded = self::load_page($x); - if($loaded) { + if ($loaded) { $content = $loaded['body']; - return [ 'content' => $content, 'success' => true ]; + return ['content' => $content, 'success' => true]; } - return [ 'success' => false ]; + return ['success' => false]; } } - + static public function compare_page($arr) { - $pageUrlName = ((array_key_exists('pageUrlName',$arr)) ? $arr['pageUrlName'] : ''); - $resource_id = ((array_key_exists('resource_id',$arr)) ? $arr['resource_id'] : ''); - $currentCommit = ((array_key_exists('currentCommit',$arr)) ? $arr['currentCommit'] : (-1)); - $compareCommit = ((array_key_exists('compareCommit',$arr)) ? $arr['compareCommit'] : 0); - $observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : ''); - $channel_id = ((array_key_exists('channel_id',$arr)) ? $arr['channel_id'] : 0); + $resource_id = ((array_key_exists('resource_id', $arr)) ? $arr['resource_id'] : ''); + $compareCommit = ((array_key_exists('compareCommit', $arr)) ? $arr['compareCommit'] : 0); + $observer_hash = ((array_key_exists('observer_hash', $arr)) ? $arr['observer_hash'] : ''); + $channel_id = ((array_key_exists('channel_id', $arr)) ? $arr['channel_id'] : 0); - $w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); + $w = NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); if (!$w['wiki']) { - return array('message' => t('Error reading wiki'), 'success' => false); + return ['message' => t('Error reading wiki'), 'success' => false]; } - $x = $arr; + $x = $arr; $x['revision'] = (-1); $currpage = self::load_page($x); - if($currpage) + if ($currpage) $currentContent = $currpage['body']; $x['revision'] = $compareCommit; - $comppage = self::load_page($x); - if($comppage) + $comppage = self::load_page($x); + if ($comppage) $compareContent = $comppage['body']; - if($currpage && $comppage) { + if ($currpage && $comppage) { require_once('library/class.Diff.php'); $diff = \Diff::toTable(\Diff::compare($currentContent, $compareContent)); - return [ 'success' => true, 'diff' => $diff ]; + return ['success' => true, 'diff' => $diff]; } - return [ 'success' => false, 'message' => t('Compare: object not found.') ]; + return ['success' => false, 'message' => t('Compare: object not found.')]; } - + static public function commit($arr) { - $commit_msg = ((array_key_exists('commit_msg', $arr)) ? $arr['commit_msg'] : t('Page updated')); - $observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : ''); - $channel_id = ((array_key_exists('channel_id',$arr)) ? $arr['channel_id'] : 0); - $pageUrlName = ((array_key_exists('pageUrlName',$arr)) ? $arr['pageUrlName'] : t('Untitled')); + $commit_msg = ((array_key_exists('commit_msg', $arr)) ? $arr['commit_msg'] : t('Page updated')); + $observer_hash = ((array_key_exists('observer_hash', $arr)) ? $arr['observer_hash'] : ''); + $channel_id = ((array_key_exists('channel_id', $arr)) ? $arr['channel_id'] : 0); - if(array_key_exists('resource_id', $arr)) { + if (array_key_exists('resource_id', $arr)) { $resource_id = $arr['resource_id']; } else { - return array('message' => t('Wiki resource_id required for git commit'), 'success' => false); + return ['message' => t('Wiki resource_id required for git commit'), 'success' => false]; } - $w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); - if (! $w['wiki']) { - return array('message' => t('Error reading wiki'), 'success' => false); + $w = NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id); + if (!$w['wiki']) { + return ['message' => t('Error reading wiki'), 'success' => false]; } $page = self::load_page($arr); - if($page) { - set_iconfig($page['id'],'nwikipage','commit_msg',escape_tags($commit_msg),true); - return [ 'success' => true, 'item_id' => $page['id'], 'page' => $page ]; + if ($page) { + set_iconfig($page['id'], 'nwikipage', 'commit_msg', escape_tags($commit_msg), true); + return ['success' => true, 'item_id' => $page['id'], 'page' => $page]; } - return [ 'success' => false, 'message' => t('Page not found.') ]; + return ['success' => false, 'message' => t('Page not found.')]; } - + static public function convert_links($s, $wikiURL) { - - if (strpos($s,'[[') !== false) { + + if (strpos($s, '[[') !== false) { preg_match_all("/\[\[(.*?)\]\]/", $s, $match); - $pages = $pageURLs = array(); + $pages = $pageURLs = []; foreach ($match[1] as $m) { // TODO: Why do we need to double urlencode for this to work? //$pageURLs[] = urlencode(urlencode(escape_tags($m))); - $titleUri = explode('|',$m); - $page = $titleUri[0] ?? ''; - $title = $titleUri[1] ?? $page; - $pageURLs[] = Zlib\NativeWiki::name_encode(escape_tags($page)); - $pages[] = $title; + $titleUri = explode('|', $m); + $page = $titleUri[0] ?? ''; + $title = $titleUri[1] ?? $page; + $pageURLs[] = NativeWiki::name_encode(escape_tags($page)); + $pages[] = $title; } $idx = 0; - while(strpos($s,'[[') !== false) { - $replace = '<a href="'.$wikiURL.'/'.$pageURLs[$idx].'">'.$pages[$idx].'</a>'; - $s = preg_replace("/\[\[(.*?)\]\]/", $replace, $s, 1); + while (strpos($s, '[[') !== false) { + $replace = '<a href="' . $wikiURL . '/' . $pageURLs[$idx] . '">' . $pages[$idx] . '</a>'; + $s = preg_replace("/\[\[(.*?)\]\]/", $replace, $s, 1); $idx++; } } @@ -564,21 +570,21 @@ class NativeWikiPage { $resource_id = ((array_key_exists('resource_id', $arr)) ? $arr['resource_id'] : ''); $pageHistory = self::page_history([ - 'channel_id' => \App::$profile_uid, + 'channel_id' => App::$profile_uid, 'observer_hash' => get_observer_hash(), 'resource_id' => $resource_id, 'pageUrlName' => $pageUrlName ]); - return replace_macros(get_markup_template('nwiki_page_history.tpl'), array( + return replace_macros(get_markup_template('nwiki_page_history.tpl'), [ '$pageHistory' => $pageHistory['history'], '$permsWrite' => $arr['permsWrite'], '$name_lbl' => t('Name'), - '$msg_label' => t('Message','wiki_history'), + '$msg_label' => t('Message', 'wiki_history'), '$date_lbl' => t('Date'), '$revert_btn' => t('Revert'), '$compare_btn' => t('Compare') - )); + ]); } @@ -590,14 +596,14 @@ class NativeWikiPage { * @return string */ static public function generate_toc($s) { - if (strpos($s,'[toc]') !== false) { + if (strpos($s, '[toc]') !== false) { //$toc_md = wiki_toc($s); // Generate Markdown-formatted list prior to HTML render $toc_md = '<ul id="wiki-toc"></ul>'; // use the available jQuery plugin http://ndabas.github.io/toc/ - $s = preg_replace("/\[toc\]/", $toc_md, $s, -1); + $s = preg_replace("/\[toc\]/", $toc_md, $s, -1); } return $s; } - + /** * Converts a select set of bbcode tags. Much of the code is copied from include/bbcode.php @@ -605,27 +611,27 @@ class NativeWikiPage { * @return string */ static public function bbcode($s) { - - $s = str_replace(array('[baseurl]', '[sitename]'), array(z_root(), get_config('system', 'sitename')), $s); - - $s = preg_replace_callback("/\[observer\.language\=(.*?)\](.*?)\[\/observer\]/ism",'oblanguage_callback', $s); - $s = preg_replace_callback("/\[observer\.language\!\=(.*?)\](.*?)\[\/observer\]/ism",'oblanguage_necallback', $s); + $s = str_replace(['[baseurl]', '[sitename]'], [z_root(), get_config('system', 'sitename')], $s); + + $s = preg_replace_callback("/\[observer\.language\=(.*?)\](.*?)\[\/observer\]/ism", 'oblanguage_callback', $s); + $s = preg_replace_callback("/\[observer\.language\!\=(.*?)\](.*?)\[\/observer\]/ism", 'oblanguage_necallback', $s); - $observer = \App::get_observer(); + + $observer = App::get_observer(); if ($observer) { - $s1 = '<span class="bb_observer" title="' . t('Different viewers will see this text differently') . '">'; - $s2 = '</span>'; + $s1 = '<span class="bb_observer" title="' . t('Different viewers will see this text differently') . '">'; + $s2 = '</span>'; $obsBaseURL = $observer['xchan_connurl']; $obsBaseURL = preg_replace("/\/poco\/.*$/", '', $obsBaseURL); - $s = str_replace('[observer.baseurl]', $obsBaseURL, $s); - $s = str_replace('[observer.url]', $observer['xchan_url'], $s); - $s = str_replace('[observer.name]', $s1 . $observer['xchan_name'] . $s2, $s); - $s = str_replace('[observer.address]', $s1 . $observer['xchan_addr'] . $s2, $s); - $s = str_replace('[observer.webname]', substr($observer['xchan_addr'], 0, strpos($observer['xchan_addr'], '@')), $s); - $s = str_replace('[observer.photo]', '', $s); - } + $s = str_replace('[observer.baseurl]', $obsBaseURL, $s); + $s = str_replace('[observer.url]', $observer['xchan_url'], $s); + $s = str_replace('[observer.name]', $s1 . $observer['xchan_name'] . $s2, $s); + $s = str_replace('[observer.address]', $s1 . $observer['xchan_addr'] . $s2, $s); + $s = str_replace('[observer.webname]', substr($observer['xchan_addr'], 0, strpos($observer['xchan_addr'], '@')), $s); + $s = str_replace('[observer.photo]', '', $s); + } else { $s = str_replace('[observer.baseurl]', '', $s); $s = str_replace('[observer.url]', '', $s); @@ -637,62 +643,63 @@ class NativeWikiPage { return $s; } - + static public function get_file_ext($arr) { - if($arr['mimetype'] === 'text/bbcode') + if ($arr['mimetype'] === 'text/bbcode') return '.bb'; - elseif($arr['mimetype'] === 'text/markdown') + elseif ($arr['mimetype'] === 'text/markdown') return '.md'; - elseif($arr['mimetype'] === 'text/plain') + elseif ($arr['mimetype'] === 'text/plain') return '.txt'; } - - // This function is derived from + + // This function is derived from // http://stackoverflow.com/questions/32068537/generate-table-of-contents-from-markdown-in-php static public function toc($content) { - // ensure using only "\n" as line-break - $source = str_replace(["\r\n", "\r"], "\n", $content); - - // look for markdown TOC items - preg_match_all( - '/^(?:=|-|#).*$/m', - $source, - $matches, - PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE - ); - - // preprocess: iterate matched lines to create an array of items - // where each item is an array(level, text) - $file_size = strlen($source); - foreach ($matches[0] as $item) { - $found_mark = substr($item[0], 0, 1); - if ($found_mark == '#') { - // text is the found item - $item_text = $item[0]; - $item_level = strrpos($item_text, '#') + 1; - $item_text = substr($item_text, $item_level); - } else { - // text is the previous line (empty if <hr>) - $item_offset = $item[1]; - $prev_line_offset = strrpos($source, "\n", -($file_size - $item_offset + 2)); - $item_text = - substr($source, $prev_line_offset, $item_offset - $prev_line_offset - 1); - $item_text = trim($item_text); - $item_level = $found_mark == '=' ? 1 : 2; - } - if (!trim($item_text) OR strpos($item_text, '|') !== FALSE) { - // item is an horizontal separator or a table header, don't mind - continue; - } - $raw_toc[] = ['level' => $item_level, 'text' => trim($item_text)]; - } + // ensure using only "\n" as line-break + $source = str_replace(["\r\n", "\r"], "\n", $content); + + // look for markdown TOC items + preg_match_all( + '/^(?:=|-|#).*$/m', + $source, + $matches, + PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE + ); + + // preprocess: iterate matched lines to create an array of items + // where each item is an array(level, text) + $file_size = strlen($source); + foreach ($matches[0] as $item) { + $found_mark = substr($item[0], 0, 1); + if ($found_mark == '#') { + // text is the found item + $item_text = $item[0]; + $item_level = strrpos($item_text, '#') + 1; + $item_text = substr($item_text, $item_level); + } + else { + // text is the previous line (empty if <hr>) + $item_offset = $item[1]; + $prev_line_offset = strrpos($source, "\n", -($file_size - $item_offset + 2)); + $item_text = + substr($source, $prev_line_offset, $item_offset - $prev_line_offset - 1); + $item_text = trim($item_text); + $item_level = $found_mark == '=' ? 1 : 2; + } + if (!trim($item_text) or strpos($item_text, '|') !== FALSE) { + // item is an horizontal separator or a table header, don't mind + continue; + } + $raw_toc[] = ['level' => $item_level, 'text' => trim($item_text)]; + } $o = ''; - foreach($raw_toc as $t) { + foreach ($raw_toc as $t) { $level = intval($t['level']); - $text = $t['text']; + $text = $t['text']; switch ($level) { case 1: $li = '* '; @@ -712,7 +719,7 @@ class NativeWikiPage { } $o .= $li . $text . "\n"; } - return $o; + return $o; } } diff --git a/Zotlabs/Lib/PConfig.php b/Zotlabs/Lib/PConfig.php index 765131f0d..80340f501 100644 --- a/Zotlabs/Lib/PConfig.php +++ b/Zotlabs/Lib/PConfig.php @@ -2,6 +2,8 @@ namespace Zotlabs\Lib; +use App; + /** * @brief Class for handling channel specific configurations. * @@ -32,15 +34,15 @@ class PConfig { if(is_null($uid) || $uid === false) return false; - if(! is_array(\App::$config)) { + if(! is_array(App::$config)) { btlogger('App::$config not an array'); } - if(! array_key_exists($uid, \App::$config)) { - \App::$config[$uid] = array(); + if(! array_key_exists($uid, App::$config)) { + App::$config[$uid] = array(); } - if(! is_array(\App::$config[$uid])) { + if(! is_array(App::$config[$uid])) { btlogger('App::$config[$uid] not an array: ' . $uid); } @@ -52,12 +54,12 @@ class PConfig { foreach($r as $rr) { $k = $rr['k']; $c = $rr['cat']; - if(! array_key_exists($c, \App::$config[$uid])) { - \App::$config[$uid][$c] = array(); - \App::$config[$uid][$c]['config_loaded'] = true; + if(! array_key_exists($c, App::$config[$uid])) { + App::$config[$uid][$c] = array(); + App::$config[$uid][$c]['config_loaded'] = true; } - \App::$config[$uid][$c][$k] = $rr['v']; - \App::$config[$uid][$c]['pcfgud:'.$k] = $rr['updated']; + App::$config[$uid][$c][$k] = $rr['v']; + App::$config[$uid][$c]['pcfgud:'.$k] = $rr['updated']; } } } @@ -86,15 +88,15 @@ class PConfig { if(is_null($uid) || $uid === false) return $default; - if(! array_key_exists($uid, \App::$config)) + if(! array_key_exists($uid, App::$config)) self::Load($uid); - if((! array_key_exists($family, \App::$config[$uid])) || (! array_key_exists($key, \App::$config[$uid][$family]))) + if((! array_key_exists($family, App::$config[$uid])) || (! array_key_exists($key, App::$config[$uid][$family]))) return $default; - return ((! is_array(\App::$config[$uid][$family][$key])) && (preg_match('|^a:[0-9]+:{.*}$|s', \App::$config[$uid][$family][$key])) - ? unserialize(\App::$config[$uid][$family][$key]) - : \App::$config[$uid][$family][$key] + return ((! is_array(App::$config[$uid][$family][$key])) && (preg_match('|^a:[0-9]+:{.*}$|s', App::$config[$uid][$family][$key])) + ? unserialize(App::$config[$uid][$family][$key]) + : App::$config[$uid][$family][$key] ); } @@ -133,6 +135,7 @@ class PConfig { $dbvalue = ((is_array($value)) ? serialize($value) : $value); $dbvalue = ((is_bool($dbvalue)) ? intval($dbvalue) : $dbvalue); $new = false; + $update = false; $now = datetime_convert(); if (! $updated) { @@ -143,23 +146,22 @@ class PConfig { $updated = datetime_convert('UTC','UTC','-2 seconds'); } - $hash = hash('sha256',$family.':'.$key); + $hash = gen_link_id($family.':'.$key); if (self::Get($uid, 'hz_delpconfig', $hash) !== false) { if (self::Get($uid, 'hz_delpconfig', $hash) > $now) { logger('Refusing to update pconfig with outdated info (Item deleted more recently).', LOGGER_NORMAL, LOG_ERR); return self::Get($uid,$family,$key); } else { - self::Delete($uid,'hz_delpconfig',$hash); + self::Delete($uid, 'hz_delpconfig', $hash); } } if(self::Get($uid, $family, $key) === false) { - if(! array_key_exists($uid, \App::$config)) - \App::$config[$uid] = array(); - if(! array_key_exists($family, \App::$config[$uid])) - \App::$config[$uid][$family] = array(); - + if(! array_key_exists($uid, App::$config)) + App::$config[$uid] = array(); + if(! array_key_exists($family, App::$config[$uid])) + App::$config[$uid][$family] = array(); $ret = q("INSERT INTO pconfig ( uid, cat, k, v, updated ) VALUES ( %d, '%s', '%s', '%s', '%s' ) ", intval($uid), @@ -177,13 +179,14 @@ class PConfig { logger("Error: Insert to pconfig failed.",LOGGER_NORMAL, LOG_ERR); } - \App::$config[$uid][$family]['pcfgud:'.$key] = $updated; + $new = true; + App::$config[$uid][$family]['pcfgud:'.$key] = $updated; } else { - $new = (\App::$config[$uid][$family]['pcfgud:'.$key] < $now); + $update = (App::$config[$uid][$family]['pcfgud:'.$key] < $now); - if ($new) { + if ($update) { // @NOTE There is still a possible race condition under limited circumstances // where a value will be updated by another thread with more current data than @@ -198,7 +201,7 @@ class PConfig { dbesc($key) ); - \App::$config[$uid][$family]['pcfgud:'.$key] = $updated; + App::$config[$uid][$family]['pcfgud:'.$key] = $updated; } else { logger('Refusing to update pconfig with outdated info.', LOGGER_NORMAL, LOG_ERR); @@ -211,16 +214,16 @@ class PConfig { // set in the life of this page. We need this to // synchronise channel clones. - if(! array_key_exists('transient', \App::$config[$uid])) - \App::$config[$uid]['transient'] = array(); - if(! array_key_exists($family, \App::$config[$uid]['transient'])) - \App::$config[$uid]['transient'][$family] = array(); + if(! array_key_exists('transient', App::$config[$uid])) + App::$config[$uid]['transient'] = array(); + if(! array_key_exists($family, App::$config[$uid]['transient'])) + App::$config[$uid]['transient'][$family] = array(); - \App::$config[$uid][$family][$key] = $value; + App::$config[$uid][$family][$key] = $value; - if ($new) { - \App::$config[$uid]['transient'][$family][$key] = $value; - \App::$config[$uid]['transient'][$family]['pcfgud:'.$key] = $updated; + if ($new || $update) { + App::$config[$uid]['transient'][$family][$key] = $value; + App::$config[$uid]['transient'][$family]['pcfgud:'.$key] = $updated; } if($ret) @@ -253,7 +256,7 @@ class PConfig { $updated = ($updated) ? $updated : datetime_convert('UTC','UTC','-2 seconds'); $now = datetime_convert(); - $newer = (\App::$config[$uid][$family]['pcfgud:'.$key] < $now); + $newer = (App::$config[$uid][$family]['pcfgud:'.$key] < $now); if (! $newer) { logger('Refusing to delete pconfig with outdated delete request.', LOGGER_NORMAL, LOG_ERR); @@ -262,12 +265,12 @@ class PConfig { $ret = false; - if (isset(\App::$config[$uid][$family][$key])) { - unset(\App::$config[$uid][$family][$key]); + if (isset(App::$config[$uid][$family][$key])) { + unset(App::$config[$uid][$family][$key]); } - if (isset(\App::$config[$uid][$family]['pcfgud:'.$key])) { - unset(\App::$config[$uid][$family]['pcfgud:'.$key]); + if (isset(App::$config[$uid][$family]['pcfgud:'.$key])) { + unset(App::$config[$uid][$family]['pcfgud:'.$key]); } $ret = q("DELETE FROM pconfig WHERE uid = %d AND cat = '%s' AND k = '%s'", @@ -278,9 +281,9 @@ class PConfig { // Synchronize delete with clones. - if ($family != 'hz_delpconfig') { - $hash = hash('sha256',$family.':'.$key); - set_pconfig($uid,'hz_delpconfig',$hash,$updated); + if ($family !== 'hz_delpconfig') { + $hash = gen_link_id($family.':'.$key); + set_pconfig($uid, 'hz_delpconfig', $hash, $updated); } return $ret; diff --git a/Zotlabs/Lib/Permcat.php b/Zotlabs/Lib/Permcat.php index ca4aed9ed..0a38ca324 100644 --- a/Zotlabs/Lib/Permcat.php +++ b/Zotlabs/Lib/Permcat.php @@ -4,6 +4,7 @@ namespace Zotlabs\Lib; use Zotlabs\Access\PermissionRoles; use Zotlabs\Access\Permissions; +use Zotlabs\Daemon\Master; /** * @brief Permission Categories. Permission rules for various classes of connections. @@ -38,33 +39,33 @@ class Permcat { // first check role perms for a perms_connect setting - $role = get_pconfig($channel_id,'system','permissions_role'); - if($role) { + $role = get_pconfig($channel_id, 'system', 'permissions_role'); + if ($role) { $x = PermissionRoles::role_perms($role); - if($x['perms_connect']) { + if ($x['perms_connect']) { $perms = Permissions::FilledPerms($x['perms_connect']); } } // if no role perms it may be a custom role, see if there any autoperms - if(! $perms) { + if (!$perms) { $perms = Permissions::FilledAutoPerms($channel_id); } // if no autoperms it may be a custom role with manual perms - if(! $perms) { + if (!$perms) { $r = q("select channel_hash from channel where channel_id = %d", intval($channel_id) ); - if($r) { + if ($r) { $x = q("select * from abconfig where chan = %d and xchan = '%s' and cat = 'my_perms'", intval($channel_id), dbesc($r[0]['channel_hash']) ); - if($x) { - foreach($x as $xv) { + if ($x) { + foreach ($x as $xv) { $perms[$xv['k']] = intval($xv['v']); } } @@ -73,25 +74,27 @@ class Permcat { // nothing was found - create a filled permission array where all permissions are 0 - if(! $perms) { + if (!$perms) { $perms = Permissions::FilledPerms([]); } $this->permcats[] = [ 'name' => 'default', - 'localname' => t('default','permcat'), + 'localname' => t('Default', 'permcat'), 'perms' => Permissions::Operms($perms), + 'raw_perms' => $perms, 'system' => 1 ]; $p = $this->load_permcats($channel_id); - if($p) { - for($x = 0; $x < count($p); $x++) { + if ($p) { + for ($x = 0; $x < count($p); $x++) { $this->permcats[] = [ 'name' => $p[$x][0], 'localname' => $p[$x][1], 'perms' => Permissions::Operms(Permissions::FilledPerms($p[$x][2])), + 'raw_perms' => Permissions::FilledPerms($p[$x][2]), 'system' => intval($p[$x][3]) ]; } @@ -116,9 +119,9 @@ class Permcat { * * \e bool \b error if $name not found in permcats true */ public function fetch($name) { - if($name && $this->permcats) { - foreach($this->permcats as $permcat) { - if(strcasecmp($permcat['name'], $name) === 0) { + if ($name && $this->permcats) { + foreach ($this->permcats as $permcat) { + if (strcasecmp($permcat['name'], $name) === 0) { return $permcat; } } @@ -128,31 +131,28 @@ class Permcat { } public function load_permcats($uid) { - + /* $permcats = [ - [ 'follower', t('follower','permcat'), - [ 'view_stream','view_profile','view_contacts','view_storage','view_pages','view_wiki', - 'post_like' ], 1 + [ 'contributor', t('Contributor','permcat'), + [ 'view_stream','view_profile','view_contacts','view_storage','view_pages', + 'write_storage','post_wall','write_pages','write_wiki','post_comments', 'post_mail', 'post_like', + 'chat' ], 1 ], - [ 'contributor', t('contributor','permcat'), + [ 'muted', t('Muted','permcat'), [ 'view_stream','view_profile','view_contacts','view_storage','view_pages','view_wiki', - 'post_wall','post_comments','write_wiki','post_like','tag_deliver','chat' ], 1 + 'post_comments','write_wiki','post_like' ], 1 ], - [ 'publisher', t('publisher','permcat'), - [ 'view_stream','view_profile','view_contacts','view_storage','view_pages', - 'write_storage','post_wall','write_pages','write_wiki','post_comments','post_like','tag_deliver', - 'chat', 'republish' ], 1 - ] ]; - - if($uid) { + */ + if ($uid) { $x = q("select * from pconfig where uid = %d and cat = 'permcat'", intval($uid) ); - if($x) { - foreach($x as $xv) { - $value = ((preg_match('|^a:[0-9]+:{.*}$|s', $xv['v'])) ? unserialize($xv['v']) : $xv['v']); - $permcats[] = [ $xv['k'], $xv['k'], $value, 0 ]; + + if ($x) { + foreach ($x as $xv) { + $value = ((preg_match('|^a:[0-9]+:{.*}$|s', $xv['v'])) ? unserialize($xv['v']) : $xv['v']); + $permcats[] = [$xv['k'], $xv['k'], $value, 0]; } } } @@ -167,11 +167,11 @@ class Permcat { } static public function find_permcat($arr, $name) { - if((! $arr) || (! $name)) + if ((!$arr) || (!$name)) return false; - foreach($arr as $p) - if($p['name'] == $name) + foreach ($arr as $p) + if ($p['name'] == $name) return $p['value']; } @@ -183,4 +183,105 @@ class Permcat { PConfig::Delete($channel_id, 'permcat', $name); } -}
\ No newline at end of file + /** + * @brief assign a contact role to contacts + * + * @param array $channel + * @param string $role the name of the role + * @param array $contacts an array of contact hashes + */ + public static function assign($channel, $role, $contacts) { + + if (!isset($channel['channel_id'])) { + return; + } + + if (!is_array($contacts) || empty($contacts)) { + return; + } + + if (!$role) { + // lookup the default + $role = get_pconfig($channel['channel_id'], 'system', 'default_permcat', 'default'); + } + + + // Doublecheck that we do not assign a role to ourself. + // It does not make a difference but could be confusing. + if (in_array($channel['channel_hash'], $contacts)) { + $contacts = array_diff($contacts, [$channel['channel_hash']]); + } + + $all_perms = Permissions::Perms(); + $permcats = new Permcat($channel['channel_id']); + $role_perms = $permcats->fetch($role); + + if (isset($role_perms['error'])) { + return false; + } + + $perms = $role_perms['raw_perms']; + + $values_sql = ''; + stringify_array_elms($contacts, true); + + if ($all_perms && $perms) { + + foreach ($contacts as $contact) { + foreach ($all_perms as $perm => $desc) { + if (array_key_exists($perm, $perms)) { + $values_sql .= " (" . intval($channel['channel_id']) . ", " . protect_sprintf($contact) . ", 'my_perms', '" . dbesc($perm) . "', " . intval($perms[$perm]) . "),"; + } + else { + $values_sql .= " (" . intval($channel['channel_id']) . ", " . protect_sprintf($contact) . ", 'my_perms', '" . dbesc($perm) . "', 0), "; + } + } + } + } + + $values_sql = rtrim($values_sql, ','); + + dbq("DELETE FROM abconfig WHERE chan = " . intval($channel['channel_id']) . " AND cat = 'my_perms' AND xchan IN (" . protect_sprintf(implode(',', $contacts)) . ")"); + + dbq("INSERT INTO abconfig ( chan, xchan, cat, k, v ) VALUES $values_sql"); + + q("UPDATE abook SET abook_role = '%s' + WHERE abook_xchan IN (" . protect_sprintf(implode(',', $contacts)) . ") AND abook_channel = %d", + dbesc($role), + intval($channel['channel_id']) + ); + + $r = q("SELECT abook.*, xchan.* FROM abook LEFT JOIN xchan ON abook.abook_xchan = xchan.xchan_hash WHERE abook.abook_xchan IN (" . protect_sprintf(implode(',', $contacts)) . ") AND abook.abook_channel = %d AND abook_self = 0", + intval($channel['channel_id']) + ); + + foreach ($r as $rr) { + + if (intval($rr['abook_self'])) { + continue; + } + + Master::Summon([ + 'Notifier', + 'permission_update', + $rr['abook_id'] + ]); + + $clone = $rr; + + unset($clone['abook_id']); + unset($clone['abook_account']); + unset($clone['abook_channel']); + + $abconfig = load_abconfig($channel['channel_id'], $clone['abook_xchan']); + if ($abconfig) + $clone['abconfig'] = $abconfig; + + Libsync::build_sync_packet(0 /* use the current local_channel */, ['abook' => [$clone]]); + + } + + return true; + } + +} diff --git a/Zotlabs/Lib/Queue.php b/Zotlabs/Lib/Queue.php index 373a7d304..e03816f05 100644 --- a/Zotlabs/Lib/Queue.php +++ b/Zotlabs/Lib/Queue.php @@ -2,6 +2,9 @@ namespace Zotlabs\Lib; +use Zotlabs\Zot6\Receiver; +use Zotlabs\Zot6\Zot6Handler; + class Queue { static function update($id, $add_priority = 0) { @@ -28,19 +31,19 @@ class Queue { $might_be_down = ((datetime_convert('UTC','UTC',$y[0]['earliest']) < datetime_convert('UTC','UTC','now - 2 days')) ? true : false); - // Set all other records for this destination way into the future. + // Set all other records for this destination way into the future. // The queue delivers by destination. We'll keep one queue item for // this destination (this one) with a shorter delivery. If we succeed // once, we'll try to deliver everything for that destination. - // The delivery will be set to at most once per hour, and if the + // The delivery will be set to at most once per hour, and if the // queue item is less than 12 hours old, we'll schedule for fifteen - // minutes. + // minutes. q("UPDATE outq SET outq_scheduled = '%s' WHERE outq_posturl = '%s'", dbesc(datetime_convert('UTC','UTC','now + 5 days')), dbesc($x[0]['outq_posturl']) ); - + $since = datetime_convert('UTC','UTC',$x[0]['outq_created']); if(($might_be_down) || ($since < datetime_convert('UTC','UTC','now - 12 hour'))) { @@ -50,9 +53,9 @@ class Queue { $next = datetime_convert('UTC','UTC','now + ' . intval($add_priority) . ' minutes'); } - q("UPDATE outq SET outq_updated = '%s', - outq_priority = outq_priority + %d, - outq_scheduled = '%s' + q("UPDATE outq SET outq_updated = '%s', + outq_priority = outq_priority + %d, + outq_scheduled = '%s' WHERE outq_hash = '%s'", dbesc(datetime_convert()), @@ -66,7 +69,7 @@ class Queue { static function remove($id,$channel_id = 0) { logger('queue: remove queue item ' . $id,LOGGER_DEBUG); $sql_extra = (($channel_id) ? " and outq_channel = " . intval($channel_id) . " " : ''); - + q("DELETE FROM outq WHERE outq_hash = '%s' $sql_extra", dbesc($id) ); @@ -75,7 +78,7 @@ class Queue { static function remove_by_posturl($posturl) { logger('queue: remove queue posturl ' . $posturl,LOGGER_DEBUG); - + q("DELETE FROM outq WHERE outq_posturl = '%s' ", dbesc($posturl) ); @@ -88,7 +91,7 @@ class Queue { $sql_extra = (($channel['channel_id']) ? " and outq_channel = " . intval($channel['channel_id']) . " " : ''); // Set the next scheduled run date so far in the future that it will be expired - // long before it ever makes it back into the delivery chain. + // long before it ever makes it back into the delivery chain. q("update outq set outq_delivered = 1, outq_updated = '%s', outq_scheduled = '%s' where outq_hash = '%s' $sql_extra ", dbesc(datetime_convert()), @@ -108,7 +111,7 @@ class Queue { } $x = q("insert into outq ( outq_hash, outq_account, outq_channel, outq_driver, outq_posturl, outq_async, outq_priority, - outq_created, outq_updated, outq_scheduled, outq_notify, outq_msg ) + outq_created, outq_updated, outq_scheduled, outq_notify, outq_msg ) values ( '%s', %d, %d, '%s', '%s', %d, %d, '%s', '%s', '%s', '%s', '%s' )", dbesc($arr['hash']), intval($arr['account_id']), @@ -133,7 +136,7 @@ class Queue { $base = null; $h = parse_url($outq['outq_posturl']); - if($h !== false) + if($h !== false) $base = $h['scheme'] . '://' . $h['host'] . (isset($h['port']) ? ':' . $h['port'] : ''); if(($base) && ($base !== z_root()) && ($immediate)) { @@ -147,7 +150,7 @@ class Queue { return; } if($y[0]['site_update'] < datetime_convert('UTC','UTC','now - 1 month')) { - self::update($outq['outq_hash'],10); + self::update($outq['outq_hash'], 10); logger('immediate delivery deferred for site ' . $base); return; } @@ -158,12 +161,12 @@ class Queue { // your site has existed. Since we don't know for sure what these sites are, // call them unknown - site_store_lowlevel( + site_store_lowlevel( [ 'site_url' => $base, 'site_update' => datetime_convert(), 'site_dead' => 0, - 'site_type' => intval(($outq['outq_driver'] === 'post') ? SITE_TYPE_NOTZOT : SITE_TYPE_UNKNOWN), + 'site_type' => SITE_TYPE_UNKNOWN, 'site_crypto' => '' ] ); @@ -171,65 +174,17 @@ class Queue { } $arr = array('outq' => $outq, 'base' => $base, 'handled' => false, 'immediate' => $immediate); - call_hooks('queue_deliver',$arr); + call_hooks('queue_deliver', $arr); if($arr['handled']) return; - // "post" queue driver - used for diaspora and friendica-over-diaspora communications. - - if($outq['outq_driver'] === 'post') { - $result = z_post_url($outq['outq_posturl'],$outq['outq_msg']); - if($result['success'] && $result['return_code'] < 300) { - logger('deliver: queue post success to ' . $outq['outq_posturl'], LOGGER_DEBUG); - if($base) { - q("update site set site_update = '%s', site_dead = 0 where site_url = '%s' ", - dbesc(datetime_convert()), - dbesc($base) - ); - } - q("update dreport set dreport_result = '%s', dreport_time = '%s' where dreport_queue = '%s'", - dbesc('accepted for delivery'), - dbesc(datetime_convert()), - dbesc($outq['outq_hash']) - ); - self::remove($outq['outq_hash']); - - // server is responding - see if anything else is going to this destination and is piled up - // and try to send some more. We're relying on the fact that do_delivery() results in an - // immediate delivery otherwise we could get into a queue loop. - - if(! $immediate) { - $x = q("select outq_hash from outq where outq_posturl = '%s' and outq_delivered = 0", - dbesc($outq['outq_posturl']) - ); - - $piled_up = array(); - if($x) { - foreach($x as $xx) { - $piled_up[] = $xx['outq_hash']; - } - } - if($piled_up) { - // call do_delivery() with the force flag - do_delivery($piled_up, true); - } - } - } - else { - logger('deliver: queue post returned ' . $result['return_code'] - . ' from ' . $outq['outq_posturl'],LOGGER_DEBUG); - self::update($outq['outq_hash'],10); - } - return; - } - // normal zot delivery logger('deliver: dest: ' . $outq['outq_posturl'], LOGGER_DEBUG); if($outq['outq_posturl'] === z_root() . '/zot') { // local delivery - $zot = new \Zotlabs\Zot6\Receiver(new \Zotlabs\Zot6\Zot6Handler(),$outq['outq_notify']); + $zot = new Receiver(new Zot6Handler(), $outq['outq_notify']); $result = $zot->run(); logger('returned_json: ' . json_encode($result,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES), LOGGER_DATA); logger('deliver: local zot delivery succeeded to ' . $outq['outq_posturl']); @@ -240,13 +195,14 @@ class Queue { $channel = null; if($outq['outq_channel']) { - $channel = channelx_by_n($outq['outq_channel']); + $channel = channelx_by_n($outq['outq_channel'], true); } $host_crypto = null; if($channel && $base) { - $h = q("select hubloc_sitekey, site_crypto from hubloc left join site on hubloc_url = site_url where site_url = '%s' and hubloc_sitekey != '' order by hubloc_id desc limit 1", + $h = q("SELECT hubloc_sitekey, site_crypto FROM hubloc LEFT JOIN site ON hubloc_url = site_url + WHERE site_url = '%s' AND hubloc_network = 'zot6' ORDER BY hubloc_id DESC LIMIT 1", dbesc($base) ); if($h) { @@ -256,7 +212,7 @@ class Queue { $msg = $outq['outq_notify']; - $result = Libzot::zot($outq['outq_posturl'],$msg,$channel,$host_crypto); + $result = Libzot::zot($outq['outq_posturl'], $msg, $channel, $host_crypto); if($result['success']) { logger('deliver: remote zot delivery succeeded to ' . $outq['outq_posturl']); @@ -265,7 +221,7 @@ class Queue { else { logger('deliver: remote zot delivery failed to ' . $outq['outq_posturl']); logger('deliver: remote zot delivery fail data: ' . print_r($result,true), LOGGER_DATA); - self::update($outq['outq_hash'],10); + self::update($outq['outq_hash'], 10); } } return; diff --git a/Zotlabs/Lib/Share.php b/Zotlabs/Lib/Share.php index b4cd5a194..81f378d0d 100644 --- a/Zotlabs/Lib/Share.php +++ b/Zotlabs/Lib/Share.php @@ -8,13 +8,13 @@ class Share { public function __construct($post_id) { - + if(! $post_id) return; - + if(! (local_channel() || remote_channel())) return; - + $r = q("SELECT * from item left join xchan on author_xchan = xchan_hash WHERE id = %d LIMIT 1", intval($post_id) ); @@ -23,26 +23,26 @@ class Share { if(($r[0]['item_private']) && ($r[0]['xchan_network'] !== 'rss')) return; - + $sql_extra = item_permissions_sql($r[0]['uid']); - + $r = q("select * from item where id = %d $sql_extra", intval($post_id) ); if(! $r) return; - + if($r[0]['mimetype'] !== 'text/bbcode') return; - + /** @FIXME eventually we want to post remotely via rpost on your home site */ // When that works remove this next bit: - + if(! local_channel()) return; xchan_query($r); - + $this->item = $r[0]; return; } @@ -66,14 +66,14 @@ class Share { 'address' => $this->item['author']['xchan_addr'], 'network' => $this->item['author']['xchan_network'], 'link' => [ - [ - 'rel' => 'alternate', - 'type' => 'text/html', + [ + 'rel' => 'alternate', + 'type' => 'text/html', 'href' => $this->item['author']['xchan_url'] ], [ - 'rel' => 'photo', - 'type' => $this->item['author']['xchan_photo_mimetype'], + 'rel' => 'photo', + 'type' => $this->item['author']['xchan_photo_mimetype'], 'href' => $this->item['author']['xchan_photo_m'] ] ] @@ -84,14 +84,14 @@ class Share { 'address' => $this->item['owner']['xchan_addr'], 'network' => $this->item['owner']['xchan_network'], 'link' => [ - [ - 'rel' => 'alternate', - 'type' => 'text/html', + [ + 'rel' => 'alternate', + 'type' => 'text/html', 'href' => $this->item['owner']['xchan_url'] ], [ - 'rel' => 'photo', - 'type' => $this->item['owner']['xchan_photo_mimetype'], + 'rel' => 'photo', + 'type' => $this->item['owner']['xchan_photo_mimetype'], 'href' => $this->item['owner']['xchan_photo_m'] ] ] @@ -117,7 +117,7 @@ class Share { $object = json_decode($this->item['obj'],true); $photo_bb = $object['body']; } - + if (strpos($this->item['body'], "[/share]") !== false) { $pos = strpos($this->item['body'], "[share"); $bb = substr($this->item['body'], $pos); @@ -126,7 +126,7 @@ class Share { "' profile='" . $this->item['author']['xchan_url'] . "' avatar='" . $this->item['author']['xchan_photo_s'] . "' link='" . $this->item['plink'] . - "' auth='" . ((in_array($this->item['author']['xchan_network'], ['zot6', 'zot'])) ? 'true' : 'false') . + "' auth='" . (($this->item['author']['xchan_network'] === 'zot6') ? 'true' : 'false') . "' posted='" . $this->item['created'] . "' message_id='" . $this->item['mid'] . "']"; diff --git a/Zotlabs/Lib/ThreadItem.php b/Zotlabs/Lib/ThreadItem.php index c0d5c001b..8d20935a1 100644 --- a/Zotlabs/Lib/ThreadItem.php +++ b/Zotlabs/Lib/ThreadItem.php @@ -2,7 +2,9 @@ namespace Zotlabs\Lib; +use App; use Zotlabs\Lib\Apps; +use Zotlabs\Access\AccessList; require_once('include/text.php'); @@ -58,6 +60,9 @@ class ThreadItem { $child = new ThreadItem($item); $this->add_child($child); } + + // performance: we have already added the children + unset($this->data['children']); } // allow a site to configure the order and content of the reaction emoji list @@ -98,11 +103,25 @@ class ThreadItem { $conv = $this->get_conversation(); $observer = $conv->get_observer(); - $lock = (((intval($item['item_private'])) || (($item['uid'] == local_channel()) && (strlen($item['allow_cid']) || strlen($item['allow_gid']) - || strlen($item['deny_cid']) || strlen($item['deny_gid'])))) - ? t('Private Message') + $acl = new AccessList(false); + $acl->set($item); + + $lock = ((intval($item['item_private']) || ($item['uid'] == local_channel() && $acl->is_private())) + ? t('Restricted message') : false); - $locktype = $item['item_private']; + + // 1 = restricted message, 2 = direct message + $locktype = intval($item['item_private']); + + if ($locktype === 2) { + $lock = t('Direct message'); + } + + // 0 = limited based on public policy + if ($item['uid'] == local_channel() && intval($item['item_private']) && !$acl->is_private() && strlen($item['public_policy'])) { + $lock = t('Public Policy'); + $locktype = 0; + } $shareable = ((($conv->get_profile_owner() == local_channel() && local_channel()) && ($item['item_private'] != 1)) ? true : false); @@ -110,6 +129,16 @@ class ThreadItem { if($item['author']['xchan_network'] === 'rss') $shareable = true; + // @fixme + // Have recently added code to properly handle polls in group reshares by redirecting all of the poll responses to the group. + // Sharing a poll using a regular embedded share is harder because the poll will need to fork. This is due to comment permissions. + // The original poll author may not accept responses from strangers. Forking the poll will receive responses from the sharer's + // followers, but there's no elegant way to merge these two sets of results together. For now, we'll disable sharing polls. + + if ($item['obj_type'] === 'Question') { + $shareable = false; + } + $privacy_warning = false; if(intval($item['item_private']) && ($item['owner']['xchan_network'] === 'activitypub')) { $recips = get_iconfig($item['parent'], 'activitypub', 'recips'); @@ -299,7 +328,7 @@ class ThreadItem { ); */ - $settings = t('Conversation Tools'); + $settings = t('Conversation Features'); } $has_bookmarks = false; @@ -367,7 +396,7 @@ class ThreadItem { call_hooks('dropdown_extras',$dropdown_extras_arr); $dropdown_extras = $dropdown_extras_arr['dropdown_extras']; - $midb64 = 'b64.' . base64url_encode($item['mid']); + $midb64 = gen_link_id($item['mid']); $mids = [ $midb64 ]; $response_mids = []; foreach($response_verbs as $v) { @@ -384,6 +413,12 @@ class ThreadItem { $pinned_items = ($allowed_type ? get_pconfig($item['uid'], 'pinned', $item['item_type'], []) : []); $pinned = ((!empty($pinned_items) && in_array($midb64, $pinned_items)) ? true : false); + $contact = []; + + if(App::$contacts && array_key_exists($item['author_xchan'], App::$contacts)) { + $contact = App::$contacts[$item['author_xchan']]; + } + $tmp_item = array( 'template' => $this->get_template(), 'mode' => $mode, @@ -401,6 +436,7 @@ class ThreadItem { 'mids' => $json_mids, 'parent' => $item['parent'], 'author_id' => (($item['author']['xchan_addr']) ? $item['author']['xchan_addr'] : $item['author']['xchan_url']), + 'author_is_group_actor' => (($item['author']['xchan_pubforum']) ? t('Forum') : ''), 'isevent' => $isevent, 'attend' => $attend, 'consensus' => $consensus, @@ -503,7 +539,9 @@ class ThreadItem { 'wait' => t('Please wait'), 'thread_level' => $thread_level, 'settings' => $settings, - 'thr_parent' => (($item['parent_mid'] != $item['thr_parent']) ? 'b64.' . base64url_encode($item['thr_parent']) : '') + 'thr_parent' => (($item['parent_mid'] != $item['thr_parent']) ? gen_link_id($item['thr_parent']) : ''), + 'contact_id' => (($contact) ? $contact['abook_id'] : '') + ); $arr = array('item' => $item, 'output' => $tmp_item); @@ -842,7 +880,7 @@ class ThreadItem { '$cipher' => $conv->get_cipher(), '$sourceapp' => \App::$sourcename, '$observer' => get_observer_hash(), - '$anoncomments' => ((($conv->get_mode() === 'channel' || $conv->get_mode() === 'display') && perm_is_allowed($conv->get_profile_owner(),'','post_comments')) ? true : false), + '$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)') ], '$anonmail' => [ 'anonmail', t('Your email address (required)') ], '$anonurl' => [ 'anonurl', t('Your website URL (optional)') ] diff --git a/Zotlabs/Lib/ThreadStream.php b/Zotlabs/Lib/ThreadStream.php index 68b2c70dd..7fe8fcc2e 100644 --- a/Zotlabs/Lib/ThreadStream.php +++ b/Zotlabs/Lib/ThreadStream.php @@ -77,7 +77,7 @@ class ThreadStream { $this->reload = $_SESSION['return_url']; break; case 'display': - // in this mode we set profile_owner after initialisation (from conversation()) and then + // in this mode we set profile_owner after initialisation (from conversation()) and then // pull some trickery which allows us to re-invoke this function afterward // it's an ugly hack so @FIXME $this->writable = perm_is_allowed($this->profile_owner,$ob_hash,'post_comments'); @@ -170,14 +170,14 @@ class ThreadStream { * Only add things that will be displayed */ - + if(($item->get_data_value('id') != $item->get_data_value('parent')) && (activity_match($item->get_data_value('verb'),ACTIVITY_LIKE) || activity_match($item->get_data_value('verb'),ACTIVITY_DISLIKE))) { return false; } $item->set_commentable(false); $ob_hash = (($this->observer) ? $this->observer['xchan_hash'] : ''); - + if(! comments_are_now_closed($item->get_data())) { if(($item->get_data_value('author_xchan') === $ob_hash) || ($item->get_data_value('owner_xchan') === $ob_hash)) $item->set_commentable(true); @@ -194,7 +194,7 @@ class ThreadStream { } if($this->mode === 'pubstream' && (! local_channel())) { $item->set_commentable(false); - } + } $item->set_conversation($this); diff --git a/Zotlabs/Lib/ZotURL.php b/Zotlabs/Lib/ZotURL.php index 6bb01fd7a..db0071f72 100644 --- a/Zotlabs/Lib/ZotURL.php +++ b/Zotlabs/Lib/ZotURL.php @@ -87,4 +87,4 @@ class ZotURL { return ids_to_array($r,'hubloc_url'); } -}
\ No newline at end of file +} diff --git a/Zotlabs/Lib/Zotfinger.php b/Zotlabs/Lib/Zotfinger.php index 840d91403..fa57ab56d 100644 --- a/Zotlabs/Lib/Zotfinger.php +++ b/Zotlabs/Lib/Zotfinger.php @@ -6,7 +6,7 @@ use Zotlabs\Web\HTTPSig; class Zotfinger { - static function exec($resource,$channel = null, $verify = true) { + static function exec($resource, $channel = null, $verify = true, $recurse = true) { if(! $resource) { return false; @@ -39,6 +39,30 @@ class Zotfinger { logger('fetch: ' . print_r($x,true)); + if (in_array(intval($x['return_code']), [ 404, 410 ]) && $recurse) { + + // The resource has been deleted or doesn't exist at this location. + // Try to find another nomadic resource for this channel and return that. + + // First, see if there's a hubloc for this site. Fetch that record to + // obtain the nomadic identity hash. Then use that to find any additional + // nomadic locations. + + $h = Activity::get_actor_hublocs($resource, 'zot6'); + if ($h) { + // mark this location deleted + hubloc_delete($h[0]); + $hubs = Activity::get_actor_hublocs($h[0]['hubloc_hash']); + if ($hubs) { + foreach ($hubs as $hub) { + if ($hub['hubloc_id_url'] !== $resource && !$hub['hubloc_deleted']) { + return self::exec($hub['hubloc_id_url'], $channel, $verify); + } + } + } + } + } + if($x['success']) { if ($verify) { $result['signature'] = HTTPSig::verify($x, EMPTY_STR, 'zot6'); |