diff options
Diffstat (limited to 'Zotlabs/Lib')
-rw-r--r-- | Zotlabs/Lib/Activity.php | 721 | ||||
-rw-r--r-- | Zotlabs/Lib/ActivityStreams.php | 25 | ||||
-rw-r--r-- | Zotlabs/Lib/Api_router.php | 10 | ||||
-rw-r--r-- | Zotlabs/Lib/Apps.php | 415 | ||||
-rw-r--r-- | Zotlabs/Lib/DB_Upgrade.php | 9 | ||||
-rw-r--r-- | Zotlabs/Lib/DReport.php | 20 | ||||
-rw-r--r-- | Zotlabs/Lib/Enotify.php | 25 | ||||
-rw-r--r-- | Zotlabs/Lib/Libsync.php | 2 | ||||
-rw-r--r-- | Zotlabs/Lib/Libzot.php | 402 | ||||
-rw-r--r-- | Zotlabs/Lib/Libzotdir.php | 4 | ||||
-rw-r--r-- | Zotlabs/Lib/MessageFilter.php | 16 | ||||
-rw-r--r-- | Zotlabs/Lib/PConfig.php | 38 | ||||
-rw-r--r-- | Zotlabs/Lib/Share.php | 1 | ||||
-rw-r--r-- | Zotlabs/Lib/ThreadItem.php | 60 | ||||
-rw-r--r-- | Zotlabs/Lib/ThreadListener.php | 53 | ||||
-rw-r--r-- | Zotlabs/Lib/ZotURL.php | 91 |
16 files changed, 1406 insertions, 486 deletions
diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php index 6ddbbb9db..232d845c7 100644 --- a/Zotlabs/Lib/Activity.php +++ b/Zotlabs/Lib/Activity.php @@ -2,36 +2,89 @@ namespace Zotlabs\Lib; -use Zotlabs\Lib\Libzot; -use Zotlabs\Lib\Libsync; -use Zotlabs\Lib\ActivityStreams; -use Zotlabs\Lib\Group; +use Zotlabs\Daemon\Master; +use Zotlabs\Zot6\HTTPSig; class Activity { static function encode_object($x) { + if(($x) && (! is_array($x)) && (substr(trim($x),0,1)) === '{' ) { $x = json_decode($x,true); } - if($x['type'] === ACTIVITY_OBJ_PERSON) { - return self::fetch_person($x); - } - if($x['type'] === ACTIVITY_OBJ_PROFILE) { - return self::fetch_profile($x); + + if(is_array($x)) { + + if(array_key_exists('asld',$x)) { + return $x['asld']; + } + + if($x['type'] === ACTIVITY_OBJ_PERSON) { + return self::fetch_person($x); + } + if($x['type'] === ACTIVITY_OBJ_PROFILE) { + return self::fetch_profile($x); + } + if(in_array($x['type'], [ ACTIVITY_OBJ_NOTE, ACTIVITY_OBJ_ARTICLE ] )) { + return self::fetch_item($x); + } + if($x['type'] === ACTIVITY_OBJ_THING) { + return self::fetch_thing($x); + } + if($x['type'] === ACTIVITY_OBJ_EVENT) { + return self::fetch_event($x); + } + if($x['type'] === ACTIVITY_OBJ_PHOTO) { + return self::fetch_image($x); + } } - if(in_array($x['type'], [ ACTIVITY_OBJ_NOTE, ACTIVITY_OBJ_ARTICLE ] )) { - return self::fetch_item($x); + + return $x; + + } + + static function fetch($url,$channel = null) { + $redirects = 0; + if(! check_siteallowed($url)) { + logger('blacklisted: ' . $url); + return null; } - if($x['type'] === ACTIVITY_OBJ_THING) { - return self::fetch_thing($x); + if(! $channel) { + $channel = get_sys_channel(); } - return $x; + logger('fetch: ' . $url, LOGGER_DEBUG); + if(strpos($url,'x-zot:') === 0) { + $x = ZotURL::fetch($url,$channel); + } + else { + $m = parse_url($url); + $headers = [ + 'Accept' => 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'Host' => $m['host'], + '(request-target)' => 'get ' . get_request_string($url), + 'Date' => datetime_convert('UTC','UTC','now','D, d M Y H:i:s') . ' UTC' + ]; + $h = HTTPSig::create_sig($headers,$channel['channel_prvkey'],channel_url($channel),false); + $x = z_fetch_url($url, true, $redirects, [ 'headers' => $h ] ); + } + + if($x['success']) { + $y = json_decode($x['body'],true); + logger('returned: ' . json_encode($y,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); + return json_decode($x['body'], true); + } + else { + logger('fetch failed: ' . $url); + } + return null; } + + static function fetch_person($x) { return self::fetch_profile($x); } @@ -93,6 +146,73 @@ class Activity { } } + + static function fetch_image($x) { + + + $ret = [ + 'type' => 'Image', + 'id' => $x['id'], + 'name' => $x['title'], + 'content' => bbcode($x['body'], [ 'cache' => true ]), + 'source' => [ 'mediaType' => 'text/bbcode', 'content' => $x['body'] ], + 'published' => datetime_convert('UTC','UTC',$x['created'],ATOM_TIME), + 'updated' => datetime_convert('UTC','UTC', $x['edited'],ATOM_TIME), + 'url' => [ + 'type' => 'Link', + 'mediaType' => $x['link'][0]['type'], + 'href' => $x['link'][0]['href'], + 'width' => $x['link'][0]['width'], + 'height' => $x['link'][0]['height'] + ] + ]; + return $ret; + } + + static function fetch_event($x) { + + // convert old Zot event objects to ActivityStreams Event objects + + if (array_key_exists('content',$x) && array_key_exists('dtstart',$x)) { + $ev = bbtoevent($x['content']); + if($ev) { + + $actor = null; + if(array_key_exists('author',$x) && array_key_exists('link',$x['author'])) { + $actor = $x['author']['link'][0]['href']; + } + $y = [ + 'type' => 'Event', + 'id' => z_root() . '/event/' . $ev['event_hash'], + 'summary' => bbcode($ev['summary'], [ 'cache' => true ]), + // RFC3339 Section 4.3 + 'startTime' => (($ev['adjust']) ? datetime_convert('UTC','UTC',$ev['dtstart'], ATOM_TIME) : datetime_convert('UTC','UTC',$ev['dtstart'],'Y-m-d\\TH:i:s-00:00')), + 'content' => bbcode($ev['description'], [ 'cache' => true ]), + 'location' => [ 'type' => 'Place', 'content' => bbcode($ev['location'], [ 'cache' => true ]) ], + 'source' => [ 'content' => format_event_bbcode($ev), 'mediaType' => 'text/bbcode' ], + 'actor' => $actor, + ]; + if(! $ev['nofinish']) { + $y['endTime'] = (($ev['adjust']) ? datetime_convert('UTC','UTC',$ev['dtend'], ATOM_TIME) : datetime_convert('UTC','UTC',$ev['dtend'],'Y-m-d\\TH:i:s-00:00')); + } + + // copy attachments from the passed object - these are already formatted for ActivityStreams + + if($x['attachment']) { + $y['attachment'] = $x['attachment']; + } + + if($actor) { + return $y; + } + } + } + + return $x; + + } + + static function encode_item_collection($items,$id,$type,$extra = null) { $ret = [ @@ -167,7 +287,7 @@ class Activity { $ret['id'] = ((strpos($i['mid'],'http') === 0) ? $i['mid'] : z_root() . '/item/' . urlencode($i['mid'])); if($i['title']) - $ret['title'] = bbcode($i['title']); + $ret['name'] = $i['title']; $ret['published'] = datetime_convert('UTC','UTC',$i['created'],ATOM_TIME); if($i['created'] !== $i['edited']) @@ -190,15 +310,15 @@ class Activity { $ret['attributedTo'] = $i['author']['xchan_url']; if($i['id'] != $i['parent']) { - $ret['inReplyTo'] = ((strpos($i['parent_mid'],'http') === 0) ? $i['parent_mid'] : z_root() . '/item/' . urlencode($i['parent_mid'])); + $ret['inReplyTo'] = ((strpos($i['thr_parent'],'http') === 0) ? $i['thr_parent'] : z_root() . '/item/' . urlencode($i['thr_parent'])); } if($i['mimetype'] === 'text/bbcode') { if($i['title']) - $ret['name'] = bbcode($i['title']); + $ret['name'] = bbcode($i['title'], [ 'cache' => true ]); if($i['summary']) - $ret['summary'] = bbcode($i['summary']); - $ret['content'] = bbcode($i['body']); + $ret['summary'] = bbcode($i['summary'], [ 'cache' => true ]); + $ret['content'] = bbcode($i['body'], [ 'cache' => true ]); $ret['source'] = [ 'content' => $i['body'], 'mediaType' => 'text/bbcode' ]; } @@ -291,7 +411,7 @@ class Activity { $ret = []; if($item['attach']) { - $atts = json_decode($item['attach'],true); + $atts = ((is_array($item['attach'])) ? $item['attach'] : json_decode($item['attach'],true)); if($atts) { foreach($atts as $att) { if(strpos($att['type'],'image')) { @@ -303,7 +423,7 @@ class Activity { } } } - + return $ret; } @@ -342,21 +462,28 @@ class Activity { $ret['type'] = 'Tombstone'; $ret['formerType'] = self::activity_obj_mapper($i['obj_type']); $ret['id'] = ((strpos($i['mid'],'http') === 0) ? $i['mid'] : z_root() . '/item/' . urlencode($i['mid'])); + $actor = self::encode_person($i['author'],false); + if($actor) + $ret['actor'] = $actor; + else + return []; return $ret; } $ret['type'] = self::activity_mapper($i['verb']); + + $ret['id'] = ((strpos($i['mid'],'http') === 0) ? $i['mid'] : z_root() . '/activity/' . urlencode($i['mid'])); if($i['title']) - $ret['name'] = html2plain(bbcode($i['title'])); + $ret['name'] = html2plain(bbcode($i['title'], [ 'cache' => true ])); if($i['summary']) - $ret['summary'] = bbcode($i['summary']); + $ret['summary'] = bbcode($i['summary'], [ 'cache' => true ]); if($ret['type'] === 'Announce') { $tmp = preg_replace('/\[share(.*?)\[\/share\]/ism',EMPTY_STR, $i['body']); - $ret['content'] = bbcode($tmp); + $ret['content'] = bbcode($tmp, [ 'cache' => true ]); $ret['source'] = [ 'content' => $i['body'], 'mediaType' => 'text/bbcode' @@ -382,7 +509,7 @@ class Activity { } if($i['id'] != $i['parent']) { - $ret['inReplyTo'] = ((strpos($i['parent_mid'],'http') === 0) ? $i['parent_mid'] : z_root() . '/item/' . urlencode($i['parent_mid'])); + $ret['inReplyTo'] = ((strpos($i['thr_parent'],'http') === 0) ? $i['thr_parent'] : z_root() . '/item/' . urlencode($i['thr_parent'])); $reply = true; if($i['item_private']) { @@ -413,10 +540,18 @@ 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); } + if($i['obj']['type'] === ACTIVITY_OBJ_PHOTO) { + $i['obj']['id'] = $i['id']; + } + $obj = self::encode_object($i['obj']); if($obj) $ret['object'] = $obj; @@ -431,6 +566,7 @@ class Activity { return []; } + if($i['target']) { if(! is_array($i['target'])) { $i['target'] = json_decode($i['target'],true); @@ -540,6 +676,12 @@ class Activity { } + + + + + + static function activity_mapper($verb) { if(strpos($verb,'/') === false) { @@ -556,6 +698,9 @@ class Activity { '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://purl.org/zot/activity/attendyes' => 'Accept', + 'http://purl.org/zot/activity/attendno' => 'Reject', + 'http://purl.org/zot/activity/attendmaybe' => 'TentativeAccept' ]; @@ -566,7 +711,7 @@ class Activity { // Reactions will just map to normal activities if(strpos($verb,ACTIVITY_REACT) !== false) - return 'Create'; + return 'emojiReaction'; if(strpos($verb,ACTIVITY_MOOD) !== false) return 'Create'; @@ -582,6 +727,70 @@ 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://purl.org/zot/activity/attendyes' => 'Accept', + 'http://purl.org/zot/activity/attendno' => 'Reject', + 'http://purl.org/zot/activity/attendmaybe' => 'TentativeAccept' + ]; + + + foreach($acts as $k => $v) { + if($verb === $v) { + return $k; + } + } + + logger('Unmapped activity: ' . $verb); + return 'Create'; + + } + + static function activity_obj_decode_mapper($obj) { + + $objs = [ + 'http://activitystrea.ms/schema/1.0/note' => 'Note', + 'http://activitystrea.ms/schema/1.0/note' => 'Article', + 'http://activitystrea.ms/schema/1.0/comment' => 'Note', + 'http://activitystrea.ms/schema/1.0/person' => 'Person', + 'http://purl.org/zot/activity/profile' => 'Profile', + 'http://activitystrea.ms/schema/1.0/photo' => 'Image', + 'http://activitystrea.ms/schema/1.0/profile-photo' => 'Icon', + 'http://activitystrea.ms/schema/1.0/event' => 'Event', + 'http://activitystrea.ms/schema/1.0/wiki' => 'Document', + 'http://purl.org/zot/activity/location' => 'Place', + 'http://purl.org/zot/activity/chessgame' => 'Game', + 'http://purl.org/zot/activity/tagterm' => 'zot:Tag', + 'http://purl.org/zot/activity/thing' => 'Object', + 'http://purl.org/zot/activity/file' => 'zot:File', + 'http://purl.org/zot/activity/mood' => 'zot:Mood', + + ]; + + foreach($objs as $k => $v) { + if($obj === $v) { + return $k; + } + } + + logger('Unmapped activity object: ' . $obj); + return 'Note'; + } + + + + static function activity_obj_mapper($obj) { if(strpos($obj,'/') === false) { @@ -678,7 +887,7 @@ class Activity { // Send an Accept back to them set_abconfig($channel['channel_id'],$person_obj['id'],'pubcrawl','their_follow_id', $their_follow_id); - \Zotlabs\Daemon\Master::Summon([ 'Notifier', 'permissions_accept', $contact['abook_id'] ]); + Master::Summon([ 'Notifier', 'permissions_accept', $contact['abook_id'] ]); return; case 'Accept': @@ -779,9 +988,9 @@ class Activity { if($my_perms && $automatic) { // send an Accept for this Follow activity - \Zotlabs\Daemon\Master::Summon([ 'Notifier', 'permissions_accept', $new_connection[0]['abook_id'] ]); + Master::Summon([ 'Notifier', 'permissions_accept', $new_connection[0]['abook_id'] ]); // Send back a Follow notification to them - \Zotlabs\Daemon\Master::Summon([ 'Notifier', 'permissions_create', $new_connection[0]['abook_id'] ]); + Master::Summon([ 'Notifier', 'permissions_create', $new_connection[0]['abook_id'] ]); } $clone = array(); @@ -972,7 +1181,7 @@ class Activity { $photos = import_xchan_photo($icon,$url); $r = q("update xchan set xchan_photo_date = '%s', xchan_photo_l = '%s', xchan_photo_m = '%s', xchan_photo_s = '%s', xchan_photo_mimetype = '%s' where xchan_hash = '%s'", - dbescdate(datetime_convert('UTC','UTC',$arr['photo_updated'])), + dbescdate(datetime_convert('UTC','UTC',$photos[5])), dbesc($photos[0]), dbesc($photos[1]), dbesc($photos[2]), @@ -1149,7 +1358,7 @@ class Activity { } if($act->obj['type'] === 'Note' && $s['attach']) { - $s['body'] .= self::bb_attach($s['attach']); + $s['body'] .= self::bb_attach($s['attach'],$s['body']); } // we will need a hook here to extract magnet links e.g. peertube @@ -1216,7 +1425,7 @@ class Activity { if($parent) { if($s['owner_xchan'] === $channel['channel_hash']) { // We are the owner of this conversation, so send all received comments back downstream - Zotlabs\Daemon\Master::Summon(array('Notifier','comment-import',$x['item_id'])); + Master::Summon(array('Notifier','comment-import',$x['item_id'])); } $r = q("select * from item where id = %d limit 1", intval($x['item_id']) @@ -1230,21 +1439,39 @@ class Activity { } + static function get_actor_bbmention($id) { - static function decode_note($act) { + $x = q("select * from hubloc left join xchan on hubloc_hash = xchan_hash where hubloc_hash = '%s' or hubloc_id_url = '%s' limit 1", + dbesc($id), + dbesc($id) + ); - $s = []; + if($x) { + return sprintf('@[zrl=%s]%s[/zrl]',$x[0]['xchan_url'],$x[0]['xchan_name']); + } + return '@{' . $id . '}'; + + } + static function decode_note($act) { - $content = self::get_content($act->obj); + $response_activity = false; + + $s = []; + 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'] = $act->id; - $s['parent_mid'] = $act->parent_id; + // ensure we store the original actor + self::actor_store($act->actor['id'],$act->actor); + $s['mid'] = $act->obj['id']; + $s['parent_mid'] = $act->parent_id; if($act->data['published']) { $s['created'] = datetime_convert('UTC','UTC',$act->data['published']); @@ -1259,110 +1486,333 @@ class Activity { $s['edited'] = datetime_convert('UTC','UTC',$act->obj['updated']); } - if(! $s['created']) - $s['created'] = datetime_convert(); - if(! $s['edited']) - $s['edited'] = $s['created']; + if(in_array($act->type, [ 'Like', 'Dislike', 'Flag', 'Block', 'Announce', 'Accept', 'Reject', 'TentativeAccept', 'emojiReaction' ])) { - if(in_array($act->type,['Announce'])) { - $root_content = self::get_content($act->raw); + $response_activity = true; - $s['title'] = self::bb_content($root_content,'name'); - $s['summary'] = self::bb_content($root_content,'summary'); - $s['body'] = (self::bb_content($root_content,'bbcode') ? : self::bb_content($root_content,'content')); + $s['mid'] = $act->id; + $s['parent_mid'] = $act->obj['id']; - if(strpos($s['body'],'[share') === false) { + // over-ride the object timestamp with the activity - // @fixme - error check and set defaults + if($act->data['published']) { + $s['created'] = datetime_convert('UTC','UTC',$act->data['published']); + } - $name = urlencode($act->obj['actor']['name']); - $profile = $act->obj['actor']['id']; - $photo = $act->obj['icon']['url']; + if($act->data['updated']) { + $s['edited'] = datetime_convert('UTC','UTC',$act->data['updated']); + } - $s['body'] .= "\r\n[share author='" . $name . - "' profile='" . $profile . - "' avatar='" . $photo . - "' link='" . $act->obj['id'] . - "' auth='" . ((is_matrix_url($act->obj['id'])) ? 'true' : 'false' ) . - "' posted='" . $act->obj['published'] . - "' message_id='" . $act->obj['id'] . - "']"; + $obj_actor = ((isset($act->obj['actor'])) ? $act->obj['actor'] : $act->get_actor('attributedTo', $act->obj)); + // ensure we store the original actor + self::actor_store($obj_actor['id'],$obj_actor); + + $mention = self::get_actor_bbmention($obj_actor['id']); + + if($act->type === 'Like') { + $content['content'] = sprintf( t('Likes %1$s\'s %2$s'),$mention,$act->obj['type']) . "\n\n" . $content['content']; } - } - else { - $s['title'] = self::bb_content($content,'name'); - $s['summary'] = self::bb_content($content,'summary'); - $s['body'] = (self::bb_content($content,'bbcode') ? : self::bb_content($content,'content')); + if($act->type === 'Dislike') { + $content['content'] = sprintf( t('Doesn\'t like %1$s\'s %2$s'),$mention,$act->obj['type']) . "\n\n" . $content['content']; + } + if($act->type === 'Accept' && $act->obj['type'] === 'Event' ) { + $content['content'] = sprintf( t('Will attend %1$s\'s %2$s'),$mention,$act->obj['type']) . "\n\n" . $content['content']; + } + if($act->type === 'Reject' && $act->obj['type'] === 'Event' ) { + $content['content'] = sprintf( t('Will not attend %1$s\'s %2$s'),$mention,$act->obj['type']) . "\n\n" . $content['content']; + } + if($act->type === 'TentativeAccept' && $act->obj['type'] === 'Event' ) { + $content['content'] = sprintf( t('May attend %1$s\'s %2$s'),$mention,$act->obj['type']) . "\n\n" . $content['content']; + } + if($act->type === 'Announce') { + $content['content'] = sprintf( t('🔁 Repeated %1$s\'s %2$s'), $mention, $act->obj['type']); + } + if ($act->type === 'emojiReaction') { + $content['content'] = (($act->tgt && $act->tgt['type'] === 'Image') ? '[img=32x32]' . $act->tgt['url'] . '[/img]' : '&#x' . $act->tgt['name'] . ';'); + } } - $s['verb'] = self::activity_mapper($act->type); + if(! $s['created']) + $s['created'] = datetime_convert(); + + if(! $s['edited']) + $s['edited'] = $s['created']; - if($act->type === 'Tombstone') { + $s['title'] = 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')); + + $s['verb'] = self::activity_decode_mapper($act->type); + + + if($act->type === 'Tombstone' || ($act->type === 'Create' && $act->obj['type'] === 'Tombstone')) { $s['item_deleted'] = 1; } - $s['obj_type'] = self::activity_obj_mapper($act->obj['type']); - $s['obj'] = $act->obj; + $s['obj_type'] = self::activity_obj_decode_mapper($act->obj['type']); + if($s['obj_type'] === ACTIVITY_OBJ_NOTE && $s['mid'] !== $s['parent_mid']) { + $s['obj_type'] = ACTIVITY_OBJ_COMMENT; + } + + if($act->obj['type'] === 'Event') { + $s['obj'] = []; + $s['obj']['asld'] = $act->obj; + $s['obj']['type'] = ACTIVITY_OBJ_EVENT; + $s['obj']['id'] = $act->obj['id']; + $s['obj']['title'] = $act->obj['summary']; + + if(strpos($act->obj['startTime'],'Z')) + $s['obj']['adjust'] = true; + else + $s['obj']['adjust'] = false; + + $s['obj']['dtstart'] = datetime_convert('UTC','UTC',$act->obj['startTime']); + if($act->obj['endTime']) + $s['obj']['dtend'] = datetime_convert('UTC','UTC',$act->obj['endTime']); + else + $s['obj']['nofinish'] = true; + $s['obj']['description'] = $act->obj['content']; + + if(array_path_exists('location/content',$act->obj)) + $s['obj']['location'] = $act->obj['location']['content']; + + } + else { + $s['obj'] = $act->obj; + } $instrument = $act->get_property_obj('instrument'); - if(! $instrument) + if((! $instrument) && (! $response_activity)) { $instrument = $act->get_property_obj('instrument',$act->obj); + } if($instrument && array_key_exists('type',$instrument) && $instrument['type'] === 'Service' && array_key_exists('name',$instrument)) { $s['app'] = escape_tags($instrument['name']); } - $a = self::decode_taxonomy($act->obj); - if($a) { - $s['term'] = $a; + + if(! $response_activity) { + $a = self::decode_taxonomy($act->obj); + if($a) { + $s['term'] = $a; + foreach($a as $b) { + if($b['ttype'] === TERM_EMOJI) { + $s['title'] = str_replace($b['term'],'[img=16x16]' . $b['url'] . '[/img]',$s['title']); + $s['summary'] = str_replace($b['term'],'[img=16x16]' . $b['url'] . '[/img]',$s['summary']); + $s['body'] = str_replace($b['term'],'[img=16x16]' . $b['url'] . '[/img]',$s['body']); + } + } + } + + $a = self::decode_attachment($act->obj); + if($a) { + $s['attach'] = $a; + } } - $a = self::decode_attachment($act->obj); - if($a) { - $s['attach'] = $a; + if($act->obj['type'] === 'Note' && $s['attach']) { + $s['body'] .= self::bb_attach($s['attach'],$s['body']); } + // we will need a hook here to extract magnet links e.g. peertube // right now just link to the largest mp4 we find that will fit in our // standard content region - if($act->obj['type'] === 'Video') { + if(! $response_activity) { + if($act->obj['type'] === 'Video') { - $vtypes = [ - 'video/mp4', - 'video/ogg', - 'video/webm' - ]; + $vtypes = [ + 'video/mp4', + 'video/ogg', + 'video/webm' + ]; - $mps = []; - if(array_key_exists('url',$act->obj) && is_array($act->obj['url'])) { - foreach($act->obj['url'] as $vurl) { - if(in_array($vurl['mimeType'], $vtypes)) { - if(! array_key_exists('width',$vurl)) { - $vurl['width'] = 0; + $mps = []; + $ptr = null; + + if(array_key_exists('url',$act->obj)) { + if(is_array($act->obj['url'])) { + if(array_key_exists(0,$act->obj['url'])) { + $ptr = $act->obj['url']; + } + else { + $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; + } + } + elseif(array_key_exists('mediaType',$vurl)) { + if(in_array($vurl['mediaType'], $vtypes)) { + if(! array_key_exists('width',$vurl)) { + $vurl['width'] = 0; + } + $mps[] = $vurl; + } + } + } + } + if($mps) { + usort($mps,[ __CLASS__, '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]'; + break; + } } - $mps[] = $vurl; + } + 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]'; } } } - if($mps) { - usort($mps,'as_vid_sort'); - foreach($mps as $m) { - if(intval($m['width']) < 500) { - $s['body'] .= "\n\n" . '[video]' . $m['href'] . '[/video]'; - break; + + if($act->obj['type'] === 'Audio') { + + $atypes = [ + 'audio/mpeg', + 'audio/ogg', + 'audio/wav' + ]; + + $ptr = null; + + if(array_key_exists('url',$act->obj)) { + if(is_array($act->obj['url'])) { + if(array_key_exists(0,$act->obj['url'])) { + $ptr = $act->obj['url']; + } + else { + $ptr = [ $act->obj['url'] ]; + } + 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]'; + 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]'; + } + } + + } + + // avoid double images from hubzilla to zap/osada + + if($act->obj['type'] === 'Image' && strpos($s['body'],'zrl=') === false) { + + $ptr = null; + + if(array_key_exists('url',$act->obj)) { + if(is_array($act->obj['url'])) { + if(array_key_exists(0,$act->obj['url'])) { + $ptr = $act->obj['url']; + } + else { + $ptr = [ $act->obj['url'] ]; + } + foreach($ptr as $vurl) { + if(strpos($s['body'],$vurl['href']) === false) { + $s['body'] .= "\n\n" . '[zmg]' . $vurl['href'] . '[/zmg]'; + break; + } + } + } + elseif(is_string($act->obj['url'])) { + if(strpos($s['body'],$act->obj['url']) === false) { + $s['body'] .= "\n\n" . '[zmg]' . $act->obj['url'] . '[/zmg]'; + } + } + } + } + + + if($act->obj['type'] === 'Page' && ! $s['body']) { + + $ptr = null; + $purl = EMPTY_STR; + + if(array_key_exists('url',$act->obj)) { + if(is_array($act->obj['url'])) { + if(array_key_exists(0,$act->obj['url'])) { + $ptr = $act->obj['url']; + } + else { + $ptr = [ $act->obj['url'] ]; + } + foreach($ptr as $vurl) { + if(array_key_exists('mediaType',$vurl) && $vurl['mediaType'] === 'text/html') { + $purl = $vurl['href']; + break; + } + elseif(array_key_exists('mimeType',$vurl) && $vurl['mimeType'] === 'text/html') { + $purl = $vurl['href']; + break; + } + } + } + elseif(is_string($act->obj['url'])) { + $purl = $act->obj['url']; + } + if($purl) { + $li = z_fetch_url(z_root() . '/linkinfo?binurl=' . bin2hex($purl)); + if($li['success'] && $li['body']) { + $s['body'] .= "\n" . $li['body']; + } + else { + $s['body'] .= "\n\n" . $purl; + } } } } } + + + if(in_array($act->obj['type'],[ 'Note','Article','Page' ])) { + $ptr = null; + + if(array_key_exists('url',$act->obj)) { + if(is_array($act->obj['url'])) { + if(array_key_exists(0,$act->obj['url'])) { + $ptr = $act->obj['url']; + } + else { + $ptr = [ $act->obj['url'] ]; + } + foreach($ptr as $vurl) { + if(array_key_exists('mediaType',$vurl) && $vurl['mediaType'] === 'text/html') { + $s['plink'] = $vurl['href']; + break; + } + } + } + elseif(is_string($act->obj['url'])) { + $s['plink'] = $act->obj['url']; + } + } + } + + if(! $s['plink']) { + $s['plink'] = $s['mid']; + } + if($act->recips && (! in_array(ACTIVITY_PUBLIC_INBOX,$act->recips))) $s['item_private'] = 1; set_iconfig($s,'activitypub','recips',$act->raw_recips); - + // @FIXME: $parent is not defined if($parent) { set_iconfig($s,'activitypub','rawmsg',$act->raw,1); } @@ -1371,8 +1821,6 @@ class Activity { } - - static function announce_note($channel,$observer_hash,$act) { $s = []; @@ -1464,7 +1912,7 @@ class Activity { $body .= self::bb_content($content,'content'); if($act->obj['type'] === 'Note' && $s['attach']) { - $body .= self::bb_attach($s['attach']); + $body .= self::bb_attach($s['attach'],$body); } $body .= "[/share]"; @@ -1495,10 +1943,11 @@ class Activity { if(is_array($x) && $x['item_id']) { + // @FIXME: $parent is not defined if($parent) { if($s['owner_xchan'] === $channel['channel_hash']) { // We are the owner of this conversation, so send all received comments back downstream - Zotlabs\Daemon\Master::Summon(array('Notifier','comment-import',$x['item_id'])); + Master::Summon(array('Notifier','comment-import',$x['item_id'])); } $r = q("select * from item where id = %d limit 1", intval($x['item_id']) @@ -1634,7 +2083,7 @@ class Activity { if($result['success']) { // if the message isn't already being relayed, notify others if(intval($parent_item['item_origin'])) - Zotlabs\Daemon\Master::Summon(array('Notifier','comment-import',$result['item_id'])); + Master::Summon(array('Notifier','comment-import',$result['item_id'])); sync_an_item($channel['channel_id'],$result['item_id']); } @@ -1642,19 +2091,26 @@ class Activity { } - static function bb_attach($attach) { + + static function bb_attach($attach,$body) { $ret = false; foreach($attach as $a) { if(strpos($a['type'],'image') !== false) { - $ret .= "\n\n" . '[img]' . $a['href'] . '[/img]'; + if(self::media_not_in_body($a['href'],$body)) { + $ret .= "\n\n" . '[img]' . $a['href'] . '[/img]'; + } } if(array_key_exists('type',$a) && strpos($a['type'], 'video') === 0) { - $ret .= "\n\n" . '[video]' . $a['href'] . '[/video]'; + if(self::media_not_in_body($a['href'],$body)) { + $ret .= "\n\n" . '[video]' . $a['href'] . '[/video]'; + } } if(array_key_exists('type',$a) && strpos($a['type'], 'audio') === 0) { - $ret .= "\n\n" . '[audio]' . $a['href'] . '[/audio]'; + if(self::media_not_in_body($a['href'],$body)) { + $ret .= "\n\n" . '[audio]' . $a['href'] . '[/audio]'; + } } } @@ -1662,16 +2118,31 @@ class Activity { } + // check for the existence of existing media link in body + + static function media_not_in_body($s,$body) { + + if((strpos($body,']' . $s . '[/img]') === false) && + (strpos($body,']' . $s . '[/zmg]') === false) && + (strpos($body,']' . $s . '[/video]') === false) && + (strpos($body,']' . $s . '[/audio]') === false)) { + return true; + } + return false; + } + static function bb_content($content,$field) { require_once('include/html2bbcode.php'); - + require_once('include/event.php'); $ret = false; if(is_array($content[$field])) { foreach($content[$field] as $k => $v) { - $ret .= '[language=' . $k . ']' . html2bbcode($v) . '[/language]'; + $ret .= html2bbcode($v); + // save this for auto-translate or dynamic filtering + // $ret .= '[language=' . $k . ']' . html2bbcode($v) . '[/language]'; } } else { @@ -1682,6 +2153,9 @@ class Activity { $ret = html2bbcode($content[$field]); } } + if($field === 'content' && $content['event'] && (! strpos($ret,'[event'))) { + $ret .= format_event_bbcode($content['event']); + } return $ret; } @@ -1690,21 +2164,52 @@ class Activity { static function get_content($act) { $content = []; - if (! $act) { + $event = null; + + if ((! $act) || (! is_array($act))) { return $content; } + if($act['type'] === 'Event') { + $adjust = false; + $event = []; + $event['event_hash'] = $act['id']; + if(array_key_exists('startTime',$act) && strpos($act['startTime'],-1,1) === 'Z') { + $adjust = true; + $event['adjust'] = 1; + $event['dtstart'] = datetime_convert('UTC','UTC',$event['startTime'] . (($adjust) ? '' : 'Z')); + } + if(array_key_exists('endTime',$act)) { + $event['dtend'] = datetime_convert('UTC','UTC',$event['endTime'] . (($adjust) ? '' : 'Z')); + } + else { + $event['nofinish'] = true; + } + } + foreach ([ 'name', 'summary', 'content' ] as $a) { if (($x = self::get_textfield($act,$a)) !== false) { $content[$a] = $x; } } + + if($event) { + $event['summary'] = html2bbcode($content['summary']); + $event['description'] = html2bbcode($content['content']); + if($event['summary'] && $event['dtstart']) { + $content['event'] = $event; + } + } + if (array_key_exists('source',$act) && array_key_exists('mediaType',$act['source'])) { if ($act['source']['mediaType'] === 'text/bbcode') { $content['bbcode'] = purify_html($act['source']['content']); } } + + + return $content; } @@ -1722,4 +2227,6 @@ class Activity { } return $content; } + + }
\ No newline at end of file diff --git a/Zotlabs/Lib/ActivityStreams.php b/Zotlabs/Lib/ActivityStreams.php index 49978031e..006744aff 100644 --- a/Zotlabs/Lib/ActivityStreams.php +++ b/Zotlabs/Lib/ActivityStreams.php @@ -263,24 +263,8 @@ class ActivityStreams { return self::fetch($url); } - static function fetch($url) { - $redirects = 0; - if(! check_siteallowed($url)) { - logger('blacklisted: ' . $url); - return null; - } - logger('fetch: ' . $url, LOGGER_DEBUG); - $x = z_fetch_url($url, true, $redirects, - [ 'headers' => [ 'Accept: application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ]]); - if($x['success']) { - $y = json_decode($x['body'],true); - logger('returned: ' . json_encode($y,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); - return json_decode($x['body'], true); - } - else { - logger('fetch failed: ' . $url); - } - return null; + static function fetch($url,$channel = null) { + return Activity::fetch($url,$channel); } static function is_an_actor($s) { @@ -335,7 +319,10 @@ class ActivityStreams { function get_compound_property($property, $base = '', $namespace = '', $first = false) { $x = $this->get_property_obj($property, $base, $namespace); if($this->is_url($x)) { - $x = $this->fetch_property($x); + $y = $this->fetch_property($x); + if (is_array($y)) { + $x = $y; + } } // verify and unpack JSalmon signature if present diff --git a/Zotlabs/Lib/Api_router.php b/Zotlabs/Lib/Api_router.php index 404678bd9..6e3f231a9 100644 --- a/Zotlabs/Lib/Api_router.php +++ b/Zotlabs/Lib/Api_router.php @@ -12,8 +12,16 @@ class Api_router { } static function find($path) { - if(array_key_exists($path,self::$routes)) + if (array_key_exists($path,self::$routes)) { return self::$routes[$path]; + } + + $with_params = dirname($path) . '/[id]'; + + if (array_key_exists($with_params,self::$routes)) { + return self::$routes[$with_params]; + } + return null; } diff --git a/Zotlabs/Lib/Apps.php b/Zotlabs/Lib/Apps.php index b13658be2..69996b49d 100644 --- a/Zotlabs/Lib/Apps.php +++ b/Zotlabs/Lib/Apps.php @@ -1,32 +1,34 @@ -<?php /** @file */ +<?php namespace Zotlabs\Lib; -/** - * Apps - * - */ - require_once('include/plugin.php'); require_once('include/channel.php'); - +/** + * @brief Apps class. + * + */ class Apps { static public $available_apps = null; static public $installed_apps = null; - static public $base_apps = null; - - + /** + * @brief + * + * @param boolean $translate (optional) default true + * @return array + */ static public function get_system_apps($translate = true) { + $ret = []; - $ret = array(); if(is_dir('apps')) $files = glob('apps/*.apd'); else $files = glob('app/*.apd'); + if($files) { foreach($files as $f) { $x = self::parse_app_description($f,$translate); @@ -50,14 +52,17 @@ class Apps { } } - call_hooks('get_system_apps',$ret); + /** + * @hooks get_system_apps + * Hook to manipulate the system apps array. + */ + call_hooks('get_system_apps', $ret); return $ret; - } static public function get_base_apps() { - return get_config('system','base_apps',[ + $x = get_config('system','base_apps',[ 'Connections', 'Network', 'Settings', @@ -65,13 +70,21 @@ class Apps { 'Channel Home', 'View Profile', 'Photos', - 'Events', + 'Calendar', 'Directory', 'Search', 'Help', 'Mail', 'Profile Photo' ]); + + /** + * @hooks get_base_apps + * Hook to manipulate the base apps array. + */ + call_hooks('get_base_apps', $x); + + return $x; } static public function import_system_apps() { @@ -79,7 +92,7 @@ class Apps { return; self::$base_apps = self::get_base_apps(); - + $apps = self::get_system_apps(false); self::$available_apps = q("select * from app where app_channel = 0"); @@ -104,6 +117,7 @@ class Apps { // $id will be boolean true or false to install an app, or an integer id to update an existing app if($id === false) continue; + if($id !== true) { // if we already installed this app, but it changed, preserve any categories we created $s = EMPTY_STR; @@ -124,16 +138,17 @@ class Apps { $app['guid'] = hash('whirlpool',$app['name']); $app['system'] = 1; self::app_install(local_channel(),$app); - } - } + } } /** - * Install the system app if no system apps have been installed, or if a new system app + * Install the system app if no system apps have been installed, or if a new system app * is discovered, or if the version of a system app changes. + * + * @param array $app + * @return boolean|int */ - static public function check_install_system_app($app) { if((! is_array(self::$available_apps)) || (! count(self::$available_apps))) { return true; @@ -157,17 +172,16 @@ class Apps { return $notfound; } - /** - * Install the system app if no system apps have been installed, or if a new system app - * is discovered, or if the version of a system app changes. + * Install the personal app if no personal apps have been installed, or if a new personal app + * is discovered, or if the version of a personal app changes. + * + * @param array $app + * @return boolean|int */ - - - static public function check_install_personal_app($app) { $installed = false; - foreach(self::$installed_apps as $iapp) { + foreach(self::$installed_apps as $iapp) { if($iapp['app_id'] == hash('whirlpool',$app['name'])) { $installed = true; if(($iapp['app_version'] != $app['version']) @@ -187,19 +201,24 @@ class Apps { return strcasecmp($a['name'],$b['name']); } - - static public function parse_app_description($f,$translate = true) { - - $ret = array(); + /** + * @brief Parse app description. + * + * @param string $f filename + * @param boolean $translate (optional) default true + * @return boolean|array + */ + static public function parse_app_description($f, $translate = true) { + $ret = []; + $matches = []; $baseurl = z_root(); - $channel = \App::get_channel(); - $address = (($channel) ? $channel['channel_address'] : ''); - + //$channel = \App::get_channel(); + //$address = (($channel) ? $channel['channel_address'] : ''); + //future expansion $observer = \App::get_observer(); - $lines = @file($f); if($lines) { @@ -208,7 +227,7 @@ class Apps { $ret[$matches[1]] = trim($matches[2]); } } - } + } if(! $ret['photo']) $ret['photo'] = $baseurl . '/' . get_default_profile_photo(80); @@ -290,36 +309,41 @@ class Apps { if($ret) { if($translate) self::translate_system_apps($ret); + return $ret; } + return false; - } + } static public function translate_system_apps(&$arr) { $apps = array( 'Apps' => t('Apps'), + 'Affinity Tool' => t('Affinity Tool'), 'Articles' => t('Articles'), 'Cards' => t('Cards'), 'Admin' => t('Site Admin'), 'Report Bug' => t('Report Bug'), 'Bookmarks' => t('Bookmarks'), 'Chatrooms' => t('Chatrooms'), + 'Content Filter' => t('Content Filter'), + 'Content Import' => t('Content Import'), 'Connections' => t('Connections'), 'Remote Diagnostics' => t('Remote Diagnostics'), 'Suggest Channels' => t('Suggest Channels'), 'Login' => t('Login'), - 'Channel Manager' => t('Channel Manager'), + 'Channel Manager' => t('Channel Manager'), 'Network' => t('Stream'), 'Settings' => t('Settings'), 'Files' => t('Files'), 'Webpages' => t('Webpages'), 'Wiki' => t('Wiki'), - 'Channel Home' => t('Channel Home'), + 'Channel Home' => t('Channel Home'), 'View Profile' => t('View Profile'), - 'Photos' => t('Photos'), - 'Events' => t('Events'), - 'Directory' => t('Directory'), + 'Photos' => t('Photos'), + 'Calendar' => t('Calendar'), + 'Directory' => t('Directory'), 'Help' => t('Help'), 'Mail' => t('Mail'), 'Mood' => t('Mood'), @@ -339,7 +363,6 @@ class Apps { 'Privacy Groups' => t('Privacy Groups'), 'Notifications' => t('Notifications'), 'Order Apps' => t('Order Apps'), - 'CalDAV' => t('CalDAV'), 'CardDAV' => t('CardDAV'), 'Channel Sources' => t('Channel Sources'), 'Guest Access' => t('Guest Access'), @@ -364,30 +387,31 @@ class Apps { if(array_key_exists($arr[$x]['name'],$apps)) { $arr[$x]['name'] = $apps[$arr[$x]['name']]; } else { - // Try to guess by app name if not in list - $arr[$x]['name'] = t(trim($arr[$x]['name'])); + // Try to guess by app name if not in list + $arr[$x]['name'] = t(trim($arr[$x]['name'])); } } } - } - - // papp is a portable app - - static public function app_render($papp,$mode = 'view') { - - /** - * modes: - * view: normal mode for viewing an app via bbcode from a conversation or page - * provides install/update button if you're logged in locally - * install: like view but does not display app-bin options if they are present - * list: normal mode for viewing an app on the app page - * no buttons are shown - * edit: viewing the app page in editing mode provides a delete button - * nav: render apps for app-bin - */ - + /** + * @brief + * + * @param array $papp + * papp is a portable app + * @param string $mode (optional) default 'view' + * Render modes: + * * \b view: normal mode for viewing an app via bbcode from a conversation or page + * provides install/update button if you're logged in locally + * * \b install: like view but does not display app-bin options if they are present + * * \b list: normal mode for viewing an app on the app page + * no buttons are shown + * * \b edit: viewing the app page in editing mode provides a delete button + * * \b nav: render apps for app-bin + * + * @return void|string Parsed HTML + */ + static public function app_render($papp, $mode = 'view') { $installed = false; if(! $papp) @@ -412,7 +436,7 @@ class Apps { $sys = get_sys_channel(); $view_channel = $sys['channel_id']; } - self::app_macros($view_channel,$papp); + self::app_macros($view_channel,$papp); } if(strpos($papp['url'], ',')) { @@ -425,7 +449,6 @@ class Apps { $papp['url'] = z_root() . ((strpos($papp['url'],'/') === 0) ? '' : '/') . $papp['url']; - foreach($papp as $k => $v) { if(strpos($v,'http') === 0 && $k != 'papp') { if(! (local_channel() && strpos($v,z_root()) === 0)) { @@ -507,7 +530,7 @@ class Apps { if($x) { $hosturl = $x['scheme'] . '://' . $x['host'] . '/'; } - } + } } $install_action = (($installed) ? t('Update') : t('Install')); @@ -590,8 +613,14 @@ class Apps { return false; } - - static public function can_delete($uid,$app) { + /** + * @brief + * + * @param mixed $uid If not set return false, otherwise no influence + * @param array $app + * @return boolean + */ + static public function can_delete($uid, $app) { if(! $uid) { return false; } @@ -599,7 +628,7 @@ class Apps { $base_apps = self::get_base_apps(); if($base_apps) { foreach($base_apps as $b) { - if($app['guid'] === hash('whirlpool',$b)) { + if($app['guid'] === hash('whirlpool', $b)) { return false; } } @@ -611,7 +640,6 @@ class Apps { static public function app_destroy($uid,$app) { if($uid && $app['guid']) { - $x = q("select * from app where app_id = '%s' and app_channel = %d limit 1", dbesc($app['guid']), intval($uid) @@ -620,7 +648,7 @@ class Apps { if(! intval($x[0]['app_deleted'])) { $x[0]['app_deleted'] = 1; if(self::can_delete($uid,$app)) { - $r = q("delete from app where app_id = '%s' and app_channel = %d", + q("delete from app where app_id = '%s' and app_channel = %d", dbesc($app['guid']), intval($uid) ); @@ -628,10 +656,15 @@ class Apps { 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 { - $r = q("update app set app_deleted = 1 where app_id = '%s' and app_channel = %d", + q("update app set app_deleted = 1 where app_id = '%s' and app_channel = %d", dbesc($app['guid']), intval($uid) ); @@ -645,22 +678,23 @@ class Apps { } } } - } - static public function app_undestroy($uid,$app) { - - // undelete a system app - + /** + * @brief Undelete a system app. + * + * @param int $uid + * @param array $app + */ + static public function app_undestroy($uid, $app) { if($uid && $app['guid']) { - $x = q("select * from app where app_id = '%s' and app_channel = %d limit 1", dbesc($app['guid']), intval($uid) ); if($x) { if($x[0]['app_system']) { - $r = q("update app set app_deleted = 0 where app_id = '%s' and app_channel = %d", + q("update app set app_deleted = 0 where app_id = '%s' and app_channel = %d", dbesc($app['guid']), intval($uid) ); @@ -669,7 +703,15 @@ class Apps { } } - static public function app_feature($uid,$app,$term) { + /** + * @brief + * + * @param int $uid + * @param array $app + * @param string $term + * @return void + */ + static public function app_feature($uid, $app, $term) { $r = q("select id from app where app_id = '%s' and app_channel = %d limit 1", dbesc($app['guid']), intval($uid) @@ -693,23 +735,37 @@ class Apps { } } - static public function app_installed($uid,$app,$bypass_filter=false) { + /** + * @brief + * + * @param int $uid + * @param array $app + * @param boolean $bypass_filter (optional) default false + * @return boolean + */ + static public function app_installed($uid, $app, $bypass_filter = false) { $r = q("select id from app where app_id = '%s' and app_channel = %d limit 1", - dbesc((array_key_exists('guid',$app)) ? $app['guid'] : ''), + dbesc((array_key_exists('guid', $app)) ? $app['guid'] : ''), intval($uid) ); - if (!$bypass_filter) { + if(!$bypass_filter) { $filter_arr = [ - 'uid'=>$uid, - 'app'=>$app, - 'installed'=>$r + 'uid' => $uid, + 'app' => $app, + 'installed' => $r ]; - call_hooks('app_installed_filter',$filter_arr); + /** + * @hooks app_installed_filter + * * \e int \b uid + * * \e array \b app + * * \e mixed \b installed - return value + */ + call_hooks('app_installed_filter', $filter_arr); $r = $filter_arr['installed']; } - return(($r) ? true : false); + return(($r) ? true : false); } @@ -725,11 +781,17 @@ class Apps { 'app'=>$app, 'installed'=>$r ]; - call_hooks('addon_app_installed_filter',$filter_arr); + /** + * @hooks addon_app_installed_filter + * * \e int \b uid + * * \e array \b app + * * \e mixed \b installed - return value + */ + call_hooks('addon_app_installed_filter', $filter_arr); $r = $filter_arr['installed']; } - return(($r) ? true : false); + return(($r) ? true : false); } static public function system_app_installed($uid,$app,$bypass_filter=false) { @@ -744,28 +806,39 @@ class Apps { 'app'=>$app, 'installed'=>$r ]; - call_hooks('system_app_installed_filter',$filter_arr); + /** + * @hooks system_app_installed_filter + * * \e int \b uid + * * \e array \b app + * * \e mixed \b installed - return value + */ + call_hooks('system_app_installed_filter', $filter_arr); $r = $filter_arr['installed']; } - return(($r) ? true : false); + return(($r) ? true : false); } - - + /** + * @brief + * + * @param int $uid + * @param boolean $deleted + * @param array $cats + * @return boolean|array + */ static public function app_list($uid, $deleted = false, $cats = []) { - if($deleted) - $sql_extra = ""; + if($deleted) + $sql_extra = ''; else - $sql_extra = " and app_deleted = 0 "; + $sql_extra = ' and app_deleted = 0 '; if($cats) { - - $cat_sql_extra = " and ( "; + $cat_sql_extra = ' and ( '; foreach($cats as $cat) { if(strpos($cat_sql_extra, 'term')) - $cat_sql_extra .= "or "; + $cat_sql_extra .= 'or '; $cat_sql_extra .= "term = '" . dbesc($cat) . "' "; } @@ -777,11 +850,13 @@ class Apps { ); if(! $r) return $r; - $sql_extra .= " and app.id in ( "; + + $sql_extra .= ' and app.id in ( '; $s = ''; foreach($r as $rr) { if($s) $s .= ','; + $s .= intval($rr['oid']); } $sql_extra .= $s . ') '; @@ -792,12 +867,26 @@ class Apps { ); if($r) { - $hookinfo = Array('uid'=>$uid,'deleted'=>$deleted,'cats'=>$cats,'apps'=>$r); - call_hooks('app_list',$hookinfo); + $hookinfo = [ + 'uid' => $uid, + 'deleted' => $deleted, + 'cats' => $cats, + 'apps' => $r, + ]; + /** + * @hooks app_list + * * \e int \b uid + * * \e boolean \b deleted + * * \e array \b cats + * * \e array \b apps - return value + */ + call_hooks('app_list', $hookinfo); $r = $hookinfo['apps']; - for($x = 0; $x < count($r); $x ++) { + + for($x = 0; $x < count($r); $x++) { if(! $r[$x]['app_system']) $r[$x]['type'] = 'personal'; + $r[$x]['term'] = q("select * from term where otype = %d and oid = %d", intval(TERM_OBJ_APP), intval($r[$x]['id']) @@ -805,15 +894,17 @@ class Apps { } } - return($r); + return $r; } - static public function app_order($uid,$apps) { + static public function app_order($uid,$apps,$menu) { if(! $apps) return $apps; - $x = (($uid) ? get_pconfig($uid,'system','app_order') : get_config('system','app_order')); + $conf = (($menu === 'nav_featured_app') ? 'app_order' : 'app_pin_order'); + + $x = (($uid) ? get_pconfig($uid,'system',$conf) : get_config('system',$conf)); if(($x) && (! is_array($x))) { $y = explode(',',$x); $y = array_map('trim',$y); @@ -835,13 +926,14 @@ class Apps { $ret[] = $ap; } } - return $ret; + return $ret; } static function find_app_in_array($name,$arr) { if(! $arr) return false; + foreach($arr as $x) { if($x['name'] === $name) { return $x; @@ -850,25 +942,38 @@ class Apps { return false; } - static function moveup($uid,$guid) { - $syslist = array(); - $list = self::app_list($uid, false, ['nav_featured_app', 'nav_pinned_app']); + /** + * @brief + * + * @param int $uid + * @param int $guid + * @param string $menu + * @return void + */ + static function moveup($uid, $guid, $menu) { + $syslist = []; + + $conf = (($menu === 'nav_featured_app') ? 'app_order' : 'app_pin_order'); + + $list = self::app_list($uid, false, [ $menu ]); if($list) { foreach($list as $li) { - $syslist[] = self::app_encode($li); + $papp = self::app_encode($li); + if($menu !== 'nav_pinned_app' && strpos($papp['categories'],'nav_pinned_app') !== false) + continue; + + $syslist[] = $papp; } } self::translate_system_apps($syslist); usort($syslist,'self::app_name_compare'); - $syslist = self::app_order($uid,$syslist); + $syslist = self::app_order($uid,$syslist,$menu); if(! $syslist) return; - $newlist = []; - foreach($syslist as $k => $li) { if($li['guid'] === $guid) { $position = $k; @@ -877,6 +982,7 @@ class Apps { } if(! $position) return; + $dest_position = $position - 1; $saved = $syslist[$dest_position]; $syslist[$dest_position] = $syslist[$position]; @@ -887,29 +993,41 @@ class Apps { $narr[] = $x['name']; } - set_pconfig($uid,'system','app_order',implode(',',$narr)); - + set_pconfig($uid,'system',$conf,implode(',',$narr)); } - static function movedown($uid,$guid) { - $syslist = array(); - $list = self::app_list($uid, false, ['nav_featured_app', 'nav_pinned_app']); + /** + * @brief + * + * @param int $uid + * @param int $guid + * @param string $menu + * @return void + */ + static function movedown($uid, $guid, $menu) { + $syslist = []; + + $conf = (($menu === 'nav_featured_app') ? 'app_order' : 'app_pin_order'); + + $list = self::app_list($uid, false, [ $menu ]); if($list) { foreach($list as $li) { - $syslist[] = self::app_encode($li); + $papp = self::app_encode($li); + if($menu !== 'nav_pinned_app' && strpos($papp['categories'],'nav_pinned_app') !== false) + continue; + + $syslist[] = $papp; } } self::translate_system_apps($syslist); usort($syslist,'self::app_name_compare'); - $syslist = self::app_order($uid,$syslist); + $syslist = self::app_order($uid,$syslist,$menu); if(! $syslist) return; - $newlist = []; - foreach($syslist as $k => $li) { if($li['guid'] === $guid) { $position = $k; @@ -918,6 +1036,7 @@ class Apps { } if($position >= count($syslist) - 1) return; + $dest_position = $position + 1; $saved = $syslist[$dest_position]; $syslist[$dest_position] = $syslist[$position]; @@ -928,8 +1047,7 @@ class Apps { $narr[] = $x['name']; } - set_pconfig($uid,'system','app_order',implode(',',$narr)); - + set_pconfig($uid,'system',$conf,implode(',',$narr)); } static public function app_decode($s) { @@ -937,8 +1055,14 @@ class Apps { return json_decode($x,true); } - - static public function app_macros($uid,&$arr) { + /** + * @brief + * + * @param int $uid + * @param[in,out] array $arr + * @return void + */ + static public function app_macros($uid, &$arr) { if(! intval($uid)) return; @@ -946,21 +1070,17 @@ class Apps { $baseurl = z_root(); $channel = channelx_by_n($uid); $address = (($channel) ? $channel['channel_address'] : ''); - + //future expansion - $observer = \App::get_observer(); - + //$observer = \App::get_observer(); + $arr['url'] = str_replace(array('$baseurl','$nick'),array($baseurl,$address),$arr['url']); $arr['photo'] = str_replace(array('$baseurl','$nick'),array($baseurl,$address),$arr['photo']); - } - - - static public function app_store($arr) { //logger('app_store: ' . print_r($arr,true)); @@ -1144,16 +1264,20 @@ class Apps { } return $ret; - } - - static public function app_encode($app,$embed = false) { - - $ret = array(); + /** + * @brief + * + * @param array $app + * @param boolean $embed (optional) default false + * @return array|string + */ + static public function app_encode($app, $embed = false) { + $ret = []; $ret['type'] = 'personal'; - + if($app['app_id']) $ret['guid'] = $app['app_id']; @@ -1186,7 +1310,7 @@ class Apps { if($app['app_price']) $ret['price'] = $app['app_price']; - + if($app['app_page']) $ret['page'] = $app['app_page']; @@ -1210,12 +1334,12 @@ class Apps { foreach($app['term'] as $t) { if($s) $s .= ','; + $s .= $t['term']; } $ret['categories'] = $s; } - if(! $embed) return $ret; @@ -1223,18 +1347,15 @@ class Apps { if(array_key_exists('categories',$ret)) unset($ret['categories']); - + $j = json_encode($ret); - return '[app]' . chunk_split(base64_encode($j),72,"\n") . '[/app]'; + return '[app]' . chunk_split(base64_encode($j),72,"\n") . '[/app]'; } static public function papp_encode($papp) { return chunk_split(base64_encode(json_encode($papp)),72,"\n"); - } } - - diff --git a/Zotlabs/Lib/DB_Upgrade.php b/Zotlabs/Lib/DB_Upgrade.php index 4038a2d53..b6e3f7b7b 100644 --- a/Zotlabs/Lib/DB_Upgrade.php +++ b/Zotlabs/Lib/DB_Upgrade.php @@ -58,10 +58,15 @@ class DB_Upgrade { $c = new $cls(); + $retval = $c->run(); if($retval != UPDATE_SUCCESS) { + + $source = t('Source code of failed update: ') . "\n\n" . @file_get_contents('Zotlabs/Update/' . $s . '.php'); + + // Prevent sending hundreds of thousands of emails by creating // a lockfile. @@ -86,7 +91,9 @@ class DB_Upgrade { '$sitename' => \App::$config['system']['sitename'], '$siteurl' => z_root(), '$update' => $x, - '$error' => sprintf( t('Update %s failed. See error logs.'), $x) + '$error' => sprintf( t('Update %s failed. See error logs.'), $x), + '$baseurl' => z_root(), + '$source' => $source ] ) ] diff --git a/Zotlabs/Lib/DReport.php b/Zotlabs/Lib/DReport.php index 21b320cac..7515d3292 100644 --- a/Zotlabs/Lib/DReport.php +++ b/Zotlabs/Lib/DReport.php @@ -87,17 +87,29 @@ class DReport { // Is the sender one of our channels? - $c = q("select channel_id from channel where channel_hash = '%s' limit 1", + $c = q("select channel_id from channel where channel_hash = '%s' or channel_portable_id = '%s' limit 1", + dbesc($dr['sender']), dbesc($dr['sender']) ); + if(! $c) return false; + // legacy zot recipients add a space and their name to the xchan. remove it if true. + + $legacy_recipient = strpos($dr['recipient'], ' '); + if($legacy_recipient !== false) { + $legacy_recipient_parts = explode(' ', $dr['recipient'], 2); + $rxchan = $legacy_recipient_parts[0]; + } + else { + $rxchan = $dr['recipient']; + } - // is the recipient one of our connections, or do we want to store every report? - $rxchan = $dr['recipient']; + // is the recipient one of our connections, or do we want to store every report? + $pcf = get_pconfig($c[0]['channel_id'],'system','dreport_store_all'); if($pcf) return true; @@ -106,7 +118,7 @@ class DReport { // So if a remote site says they can't find us, that's no big surprise // and just creates a lot of extra report noise - if(($dr['location'] !== z_root()) && ($dr['sender'] === $rxchan) && ($dr['status'] === 'recipient_not_found')) + if(($dr['location'] !== z_root()) && ($dr['sender'] === $rxchan) && ($dr['status'] === 'recipient not found')) return false; // If you have a private post with a recipient list, every single site is going to report diff --git a/Zotlabs/Lib/Enotify.php b/Zotlabs/Lib/Enotify.php index 25c96d9cc..a7082f45a 100644 --- a/Zotlabs/Lib/Enotify.php +++ b/Zotlabs/Lib/Enotify.php @@ -754,9 +754,9 @@ class Enotify { // generate a multipart/alternative message header $messageHeader = $params['additionalMailHeader'] . - "From: $fromName <{$params['fromEmail']}>\n" . - "Reply-To: $fromName <{$params['replyTo']}>\n" . - "MIME-Version: 1.0\n" . + "From: $fromName <{$params['fromEmail']}>" . PHP_EOL . + "Reply-To: $fromName <{$params['replyTo']}>" . 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 @@ -764,15 +764,15 @@ class Enotify { $htmlBody = chunk_split(base64_encode($params['htmlVersion'])); $multipartMessageBody = - "--" . $mimeBoundary . "\n" . // plain text section - "Content-Type: text/plain; charset=UTF-8\n" . - "Content-Transfer-Encoding: base64\n\n" . - $textBody . "\n" . - "--" . $mimeBoundary . "\n" . // text/html section - "Content-Type: text/html; charset=UTF-8\n" . - "Content-Transfer-Encoding: base64\n\n" . - $htmlBody . "\n" . - "--" . $mimeBoundary . "--\n"; // message ending + "--" . $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 . + $htmlBody . PHP_EOL . + "--" . $mimeBoundary . "--" . PHP_EOL; // message ending // send the message $res = mail( @@ -828,6 +828,7 @@ class Enotify { $x = array( 'notify_link' => $item['llink'], 'name' => $item['author']['xchan_name'], + 'addr' => (($item['author']['xchan_addr']) ? $item['author']['xchan_addr'] : $item['author']['xchan_url']), 'url' => $item['author']['xchan_url'], 'photo' => $item['author']['xchan_photo_s'], 'when' => relative_date(($edit)? $item['edited'] : $item['created']), diff --git a/Zotlabs/Lib/Libsync.php b/Zotlabs/Lib/Libsync.php index d037a0058..d93270bc5 100644 --- a/Zotlabs/Lib/Libsync.php +++ b/Zotlabs/Lib/Libsync.php @@ -336,7 +336,7 @@ class Libsync { $disallowed = array('abook_id','abook_account','abook_channel','abook_rating','abook_rating_text','abook_not_here'); - $fields = db_columns($abook); + $fields = db_columns('abook'); foreach($arr['abook'] as $abook) { diff --git a/Zotlabs/Lib/Libzot.php b/Zotlabs/Lib/Libzot.php index 70f013eb7..9bf987027 100644 --- a/Zotlabs/Lib/Libzot.php +++ b/Zotlabs/Lib/Libzot.php @@ -2,11 +2,6 @@ namespace Zotlabs\Lib; -/** - * @brief lowlevel implementation of Zot6 protocol. - * - */ - use Zotlabs\Zot6\HTTPSig; use Zotlabs\Access\Permissions; use Zotlabs\Access\PermissionLimits; @@ -14,14 +9,17 @@ use Zotlabs\Daemon\Master; require_once('include/crypto.php'); - +/** + * @brief Lowlevel implementation of Zot6 protocol. + * + */ class Libzot { /** * @brief Generates a unique string for use as a zot guid. * - * Generates a unique string for use as a zot guid using our DNS-based url, the - * channel nickname and some entropy. + * Generates a unique string for use as a zot guid using our DNS-based url, + * the channel nickname and some entropy. * The entropy ensures uniqueness against re-installs where the same URL and * nickname are chosen. * @@ -32,9 +30,8 @@ class Libzot { * immediate universe. * * @param string $channel_nick a unique nickname of controlling entity - * @returns string + * @return string */ - static function new_uid($channel_nick) { $rawstr = z_root() . '/' . $channel_nick . '.' . mt_rand(); return(base64url_encode(hash('whirlpool', $rawstr, true), true)); @@ -52,8 +49,8 @@ class Libzot { * * @param string $guid * @param string $pubkey + * @return string */ - static function make_xchan_hash($guid, $pubkey) { return base64url_encode(hash('whirlpool', $guid . $pubkey, true)); } @@ -65,10 +62,8 @@ class Libzot { * should only be used by channels which are defined on this hub. * * @param string $hash - xchan_hash - * @returns array of hubloc (hub location structures) - * + * @return array of hubloc (hub location structures) */ - static function get_hublocs($hash) { /* Only search for active hublocs - e.g. those that haven't been marked deleted */ @@ -92,16 +87,17 @@ class Libzot { * packet type: one of 'ping', 'pickup', 'purge', 'refresh', 'keychange', 'force_refresh', 'notify', 'auth_check' * @param array $recipients * envelope recipients, array of portable_id's; empty for public posts - * @param string msg + * @param string $msg * optional message + * @param string $encoding + * optional encoding, default 'activitystreams' * @param string $remote_key * optional public site key of target hub used to encrypt entire packet * NOTE: remote_key and encrypted packets are required for 'auth_check' packets, optional for all others * @param string $methods - * optional comma separated list of encryption methods @ref self::best_algorithm() + * optional comma separated list of encryption methods @ref best_algorithm() * @returns string json encoded zot packet */ - static function build_packet($channel, $type = 'activity', $recipients = null, $msg = '', $encoding = 'activitystreams', $remote_key = null, $methods = '') { $sig_method = get_config('system','signature_algorithm','sha256'); @@ -146,11 +142,10 @@ class Libzot { * @brief Choose best encryption function from those available on both sites. * * @param string $methods - * comma separated list of encryption methods + * Comma separated list of encryption methods * @return string first match from our site method preferences crypto_methods() array - * of a method which is common to both sites; or 'aes256cbc' if no matches are found. + * of a method which is common to both sites; or 'aes256cbc' if no matches are found. */ - static function best_algorithm($methods) { $x = [ @@ -164,7 +159,6 @@ class Libzot { * * \e string \b methods - comma separated list of encryption methods * * \e string \b result - the algorithm to return */ - call_hooks('zot_best_algorithm', $x); if($x['result']) @@ -190,7 +184,7 @@ class Libzot { /** - * @brief send a zot message + * @brief Send a zot message. * * @see z_post_url() * @@ -200,18 +194,17 @@ class Libzot { * @param array $crypto (required if encrypted httpsig, requires hubloc_sitekey and site_crypto elements) * @return array see z_post_url() for returned data format */ - static function zot($url, $data, $channel = null,$crypto = null) { if($channel) { - $headers = [ - 'X-Zot-Token' => random_string(), - 'Digest' => HTTPSig::generate_digest_header($data), + $headers = [ + 'X-Zot-Token' => random_string(), + 'Digest' => HTTPSig::generate_digest_header($data), 'Content-type' => 'application/x-zot+json', '(request-target)' => 'post ' . get_request_string($url) ]; - $h = HTTPSig::create_sig($headers,$channel['channel_prvkey'],channel_url($channel),false,'sha512', + $h = HTTPSig::create_sig($headers,$channel['channel_prvkey'],channel_url($channel),false,'sha512', (($crypto) ? [ 'key' => $crypto['hubloc_sitekey'], 'algorithm' => self::best_algorithm($crypto['site_crypto']) ] : false)); } else { @@ -227,7 +220,6 @@ class Libzot { /** * @brief Refreshes after permission changed or friending, etc. * - * * refresh is typically invoked when somebody has changed permissions of a channel and they are notified * to fetch new permissions via a finger/discovery operation. This may result in a new connection * (abook entry) being added to a local channel and it may result in auto-permissions being granted. @@ -251,7 +243,6 @@ class Libzot { * * \b true if successful * * otherwise \b false */ - static function refresh($them, $channel = null, $force = false) { logger('them: ' . print_r($them,true), LOGGER_DATA, LOG_DEBUG); @@ -265,13 +256,13 @@ class Libzot { } else { $r = null; - + // if they re-installed the server we could end up with the wrong record - pointing to the old install. // We'll order by reverse id to try and pick off the newest one first and hopefully end up with the // correct hubloc. If this doesn't work we may have to re-write this section to try them all. if(array_key_exists('xchan_addr',$them) && $them['xchan_addr']) { - $r = q("select hubloc_id_url, hubloc_primary from hubloc where hubloc_addr = '%s' order by hubloc_id desc", + $r = q("select hubloc_id_url, hubloc_primary from hubloc where hubloc_addr = '%s' and hubloc_network = 'zot6' order by hubloc_id desc", dbesc($them['xchan_addr']) ); } @@ -317,7 +308,7 @@ class Libzot { if(! $hsig_valid) { logger('http signature not valid: ' . print_r($hsig,true)); - return $result; + return false; } @@ -356,7 +347,7 @@ class Libzot { ); if($r) { -logger('4'); + // connection exists // if the dob is the same as what we have stored (disregarding the year), keep the one @@ -397,9 +388,7 @@ logger('4'); } } - $closeness = get_pconfig($channel['channel_id'],'system','new_abook_closeness'); - if($closeness === false) - $closeness = 80; + $closeness = get_pconfig($channel['channel_id'],'system','new_abook_closeness',80); $y = abook_store_lowlevel( [ @@ -418,7 +407,7 @@ logger('4'); if($y) { logger("New introduction received for {$channel['channel_name']}"); $new_perms = get_all_perms($channel['channel_id'],$x['hash'],false); - + // Send a clone sync packet and a permissions update if permissions have changed $new_connection = q("select * from abook left join xchan on abook_xchan = xchan_hash where abook_xchan = '%s' and abook_channel = %d and abook_self = 0 order by abook_created desc limit 1", @@ -526,10 +515,14 @@ logger('4'); return false; } - - - - static function valid_hub($sender,$site_id) { + /** + * @brief + * + * @param string $sender + * @param string $site_id + * @return null|array + */ + static function valid_hub($sender, $site_id) { $r = q("select hubloc.*, site.site_crypto from hubloc left join site on hubloc_url = site_url where hubloc_hash = '%s' and hubloc_site_id = '%s' limit 1", dbesc($sender), @@ -550,7 +543,6 @@ logger('4'); } return $r[0]; - } /** @@ -561,21 +553,14 @@ logger('4'); * origination address. This will fetch the discovery packet of the sender, * which contains the public key we need to verify our guid and url signatures. * - * @param array $arr an associative array which must contain: - * * \e string \b guid => guid of conversant - * * \e string \b guid_sig => guid signed with conversant's private key - * * \e string \b url => URL of the origination hub of this communication - * * \e string \b url_sig => URL signed with conversant's private key + * @param string $id * * @return array An associative array with - * * \b success boolean true or false - * * \b message (optional) error string only if success is false + * * \e boolean \b success + * * \e string \b message (optional, unused) error string only if success is false */ - static function register_hub($id) { - $id_hash = false; - $valid = false; $hsig_valid = false; $result = [ 'success' => false ]; @@ -700,9 +685,27 @@ logger('4'); $adult_changed = 1; if(intval($r[0]['xchan_deleted']) != intval($arr['deleted'])) $deleted_changed = 1; + + // new style 6-MAR-2019 + + if(array_key_exists('channel_type',$arr)) { + if($arr['channel_type'] === 'collection') { + // do nothing at this time. + } + elseif($arr['channel_type'] === 'group') { + $arr['public_forum'] = 1; + } + else { + $arr['public_forum'] = 0; + } + } + + // old style + if(intval($r[0]['xchan_pubforum']) != intval($arr['public_forum'])) $pubforum_changed = 1; + if($arr['protocols']) { $protocols = implode(',',$arr['protocols']); if($protocols !== 'zot6') { @@ -809,7 +812,7 @@ logger('4'); // If setting for the default profile, unset the profile photo flag from any other photos I own if($is_default_profile) { - q("UPDATE photo SET photo_usage = %d WHERE photo_usage = %d AND resource_id != '%s' AND aid = %d AND uid = %d", + q("UPDATE photo SET photo_usage = %d WHERE photo_usage = %d AND resource_id != '%s' AND aid = %d AND uid = %d", intval(PHOTO_NORMAL), intval(PHOTO_PROFILE), dbesc($hash), @@ -956,8 +959,8 @@ logger('4'); * @param string $hub - url of site we just contacted * @param array $arr - output of z_post_url() * @param array $outq - The queue structure attached to this request + * @return void */ - static function process_response($hub, $arr, $outq) { logger('remote: ' . print_r($arr,true),LOGGER_DATA); @@ -988,7 +991,7 @@ logger('4'); if(! $x['success']) { // handle remote validation issues - + $b = q("update dreport set dreport_result = '%s', dreport_time = '%s' where dreport_queue = '%s'", dbesc(($x['message']) ? $x['message'] : 'unknown delivery error'), dbesc(datetime_convert()), @@ -996,10 +999,20 @@ logger('4'); ); } - if(is_array($x) && array_key_exists('delivery_report',$x) && is_array($x['delivery_report'])) { + if(is_array($x) && array_key_exists('delivery_report',$x) && is_array($x['delivery_report'])) { + foreach($x['delivery_report'] as $xx) { call_hooks('dreport_process',$xx); if(is_array($xx) && array_key_exists('message_id',$xx) && DReport::is_storable($xx)) { + + // legacy recipients add a space and their name to the xchan. split those if true. + $legacy_recipient = strpos($xx['recipient'], ' '); + if($legacy_recipient !== false) { + $legacy_recipient_parts = explode(' ', $xx['recipient'], 2); + $xx['recipient'] = $legacy_recipient_parts[0]; + $xx['name'] = $legacy_recipient_parts[1]; + } + q("insert into dreport ( dreport_mid, dreport_site, dreport_recip, dreport_name, dreport_result, dreport_time, dreport_xchan ) values ( '%s', '%s', '%s','%s','%s','%s','%s' ) ", dbesc($xx['message_id']), dbesc($xx['location']), @@ -1075,11 +1088,6 @@ logger('4'); * * @param array $arr * 'pickup' structure returned from remote site - * @param string $sender_url - * the url specified by the sender in the initial communication. - * We will verify the sender and url in each returned message structure and - * also verify that all the messages returned match the site url that we are - * currently processing. * * @returns array * Suitable for logging remotely, enumerating the processing results of each message/recipient combination @@ -1087,7 +1095,6 @@ logger('4'); * * [1] => \e string $delivery_status * * [2] => \e string $address */ - static function import($arr) { $env = $arr; @@ -1109,7 +1116,7 @@ logger('4'); $has_data = array_key_exists('data',$env) && $env['data']; $data = (($has_data) ? $env['data'] : false); - $AS = null; + $AS = null; if($env['encoding'] === 'activitystreams') { @@ -1118,9 +1125,14 @@ logger('4'); logger('Activity rejected: ' . print_r($data,true)); return; } - $arr = Activity::decode_note($AS); + if (is_array($AS->obj)) { + $arr = Activity::decode_note($AS); + } + else { + $arr = []; + } - logger($AS->debug()); + logger($AS->debug(),LOGGER_DATA); } @@ -1167,7 +1179,6 @@ logger('4'); $deliveries = self::public_recips($env,$AS); - } $deliveries = array_unique($deliveries); @@ -1186,31 +1197,33 @@ logger('4'); //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']) - ); + ); if($r) { - $arr['author_xchan'] = $r[0]['hubloc_hash']; + // selects a zot6 hash if available, otherwise use whatever we have + $r = self::zot_record_preferred($r); + $arr['author_xchan'] = $r['hubloc_hash']; } - $s = q("select hubloc_hash from hubloc where hubloc_id_url = '%s' limit 1", + $s = q("select hubloc_hash 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']; } else { - $arr['owner_xchan'] = $env['sender']; + $arr['owner_xchan'] = $env['sender']; } if($private) { $arr['item_private'] = true; } - // @fixme - spoofable + /// @FIXME - spoofable if($AS->data['hubloc']) { $arr['item_verified'] = true; } @@ -1224,7 +1237,7 @@ logger('4'); $relay = (($env['type'] === 'response') ? true : false ); - $result = self::process_delivery($env['sender'],$arr,$deliveries,$relay,false,$message_request); + $result = self::process_delivery($env['sender'],$AS,$arr,$deliveries,$relay,false,$message_request); } elseif($env['type'] === 'sync') { // $arr = get_channelsync_elements($data); @@ -1239,12 +1252,19 @@ logger('4'); } if ($result) { $return = array_merge($return, $result); - } + } return $return; } - static function is_top_level($env,$act) { + /** + * @brief + * + * @param array $env + * @param object $act + * @return boolean + */ + static function is_top_level($env, $act) { if($env['encoding'] === 'zot' && array_key_exists('flags',$env) && in_array('thread_parent', $env['flags'])) { return true; } @@ -1287,9 +1307,9 @@ logger('4'); * Some of these will be rejected, but this gives us a place to start. * * @param array $msg - * @return NULL|array + * @param object $act + * @return array */ - static function public_recips($msg, $act) { require_once('include/channel.php'); @@ -1390,7 +1410,7 @@ logger('4'); /** * @brief * - * @param array $sender + * @param string $sender * @param array $arr * @param array $deliveries * @param boolean $relay @@ -1399,7 +1419,7 @@ logger('4'); * @return array */ - static function process_delivery($sender, $arr, $deliveries, $relay, $public = false, $request = false) { + static function process_delivery($sender, $act, $arr, $deliveries, $relay, $public = false, $request = false) { $result = []; @@ -1418,7 +1438,7 @@ logger('4'); $DR = new DReport(z_root(),$sender,$d,$arr['mid']); - $channel = channelx_by_hash($d); + $channel = channelx_by_portid($d); if (! $channel) { $DR->update('recipient not found'); @@ -1428,13 +1448,31 @@ logger('4'); $DR->set_name($channel['channel_name'] . ' <' . channel_reddress($channel) . '>'); + if(($act) && ($act->obj) && (! is_array($act->obj))) { + // The initial object fetch failed using the sys channel credentials. + // Try again using the delivery channel credentials. + // We will also need to re-parse the $item array, + // but preserve any values that were set during anonymous parsing. + + $o = Activity::fetch($act->obj,$channel); + if($o) { + $act->obj = $o; + $arr = array_merge(Activity::decode_note($act),$arr); + } + else { + $DR->update('Incomplete or corrupt activity'); + $result[] = $DR->get(); + continue; + } + } + /** * We need to block normal top-level message delivery from our clones, as the delivered * message doesn't have ACL information in it as the cloned copy does. That copy * will normally arrive first via sync delivery, but this isn't guaranteed. * There's a chance the current delivery could take place before the cloned copy arrives * hence the item could have the wrong ACL and *could* be used in subsequent deliveries or - * access checks. + * access checks. */ if($sender === $channel['channel_portable_id'] && $arr['author_xchan'] === $channel['channel_portable_id'] && $arr['mid'] === $arr['parent_mid']) { @@ -1489,14 +1527,37 @@ logger('4'); intval($channel['channel_id']) ); if ($parent) { - $allowed = can_comment_on_post($d,$parent[0]); + $allowed = can_comment_on_post($sender,$parent[0]); } } - if($request) { - $allowed = true; + + if ($request) { + + // Conversation fetches (e.g. $request == true) take place for + // a) new comments on expired posts + // b) hyperdrive (friend-of-friend) conversations + // c) Repeats of posts by others + + + // over-ride normal connection permissions for hyperdrive (friend-of-friend) conversations + // (if hyperdrive is enabled) and repeated posts by a friend. + // If $allowed is already true, this is probably the conversation of a direct friend or a + // conversation fetch for a new comment on an expired post + // Comments of all these activities are allowed and will only be rejected (later) if the parent + // doesn't exist. + + if ($perm === 'send_stream') { + if (get_pconfig($channel['channel_id'],'system','hyperdrive',false) || $arr['verb'] === ACTIVITY_SHARE) { + $allowed = true; + } + } + else { + $allowed = true; + } + $friendofriend = true; } - + if (! $allowed) { logger("permission denied for delivery to channel {$channel['channel_id']} {$channel['channel_address']}"); $DR->update('permission denied'); @@ -1505,14 +1566,18 @@ logger('4'); } } - if($arr['mid'] != $arr['parent_mid']) { + // logger('item: ' . print_r($arr,true), LOGGER_DATA); + + if($arr['mid'] !== $arr['parent_mid']) { + + logger('checking source: "' . $arr['mid'] . '" != "' . $arr['parent_mid'] . '"'); // check source route. // We are only going to accept comments from this sender if the comment has the same route as the top-level-post, // this is so that permissions mismatches between senders apply to the entire conversation // As a side effect we will also do a preliminary check that we have the top-level-post, otherwise // processing it is pointless. - + $r = q("select route, id, owner_xchan, item_private from item where mid = '%s' and uid = %d limit 1", dbesc($arr['parent_mid']), intval($channel['channel_id']) @@ -1527,10 +1592,7 @@ logger('4'); // have the copy and we don't want the request to loop. // Also don't do this if this comment came from a conversation request packet. // It's possible that comments are allowed but posting isn't and that could - // cause a conversation fetch loop. We can detect these packets since they are - // delivered via a 'notify' packet type that has a message_id element in the - // initial zot packet (just like the corresponding 'request' packet type which - // makes the request). + // cause a conversation fetch loop. // We'll also check the send_stream permission - because if it isn't allowed, // the top level post is unlikely to be imported and // this is just an exercise in futility. @@ -1541,14 +1603,14 @@ logger('4'); } continue; } - + if($relay || $friendofriend || (intval($r[0]['item_private']) === 0 && intval($arr['item_private']) === 0)) { // reset the route in case it travelled a great distance upstream // use our parent's route so when we go back downstream we'll match // with whatever route our parent has. // Also friend-of-friend conversations may have been imported without a route, // but we are now getting comments via listener delivery - // and if there is no privacy on this or the parent, we don't care about the route, + // and if there is no privacy on this or the parent, we don't care about the route, // so just set the owner and route accordingly. $arr['route'] = $r[0]['route']; $arr['owner_xchan'] = $r[0]['owner_xchan']; @@ -1602,13 +1664,13 @@ logger('4'); // remove_community_tag is a no-op if this isn't a community tag activity self::remove_community_tag($sender,$arr,$channel['channel_id']); - + // set these just in case we need to store a fresh copy of the deleted post. // This could happen if the delete got here before the original post did. $arr['aid'] = $channel['channel_account_id']; $arr['uid'] = $channel['channel_id']; - + $item_id = self::delete_imported_item($sender,$arr,$channel['channel_id'],$relay); $DR->update(($item_id) ? 'deleted' : 'delete_failed'); $result[] = $DR->get(); @@ -1704,7 +1766,7 @@ logger('4'); * * \e array \b item * * \e array \b sender * * \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) @@ -1769,17 +1831,17 @@ logger('4'); logger($AS->debug()); - $r = q("select hubloc_hash from hubloc where hubloc_id_url = '%s' limit 1", + $r = q("select hubloc_hash from hubloc where hubloc_id_url = '%s' and hubloc_network = 'zot6' limit 1", dbesc($AS->actor['id']) - ); + ); 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 from hubloc where hubloc_id_url = '%s' and hubloc_network = 'zot6' limit 1", dbesc($AS->actor['id']) ); - } + } if(! $r) { logger('FOF Activity: no actor'); continue; @@ -1799,9 +1861,9 @@ logger('4'); $arr['author_xchan'] = $r[0]['hubloc_hash']; } - $s = q("select hubloc_hash from hubloc where hubloc_id_url = '%s' limit 1", + $s = q("select hubloc_hash from hubloc where hubloc_id_url = '%s' and hubloc_network = 'zot6' limit 1", dbesc($a['signature']['signer']) - ); + ); if($s) { $arr['owner_xchan'] = $s[0]['hubloc_hash']; @@ -1810,7 +1872,8 @@ logger('4'); $arr['owner_xchan'] = $a['signature']['signer']; } - // @fixme - spoofable + + /// @FIXME - spoofable if($AS->data['hubloc']) { $arr['item_verified'] = true; } @@ -1821,10 +1884,10 @@ logger('4'); logger('FOF Activity received: ' . print_r($arr,true), LOGGER_DATA, LOG_DEBUG); logger('FOF Activity recipient: ' . $channel['channel_portable_id'], LOGGER_DATA, LOG_DEBUG); - $result = self::process_delivery($arr['owner_xchan'],$arr, [ $channel['channel_portable_id'] ],false,false,true); + $result = self::process_delivery($arr['owner_xchan'],$AS, $arr, [ $channel['channel_portable_id'] ],false,false,true); if ($result) { $ret = array_merge($ret, $result); - } + } } return $ret; @@ -1834,15 +1897,14 @@ logger('4'); /** * @brief Remove community tag. * - * @param array $sender an associative array with - * * \e string \b hash a xchan_hash + * @param string $sender * @param array $arr an associative array * * \e int \b verb * * \e int \b obj_type * * \e int \b mid * @param int $uid + * @return void */ - static function remove_community_tag($sender, $arr, $uid) { if(! (activity_match($arr['verb'], ACTIVITY_TAG) && ($arr['obj_type'] == ACTIVITY_OBJ_TAGTERM))) @@ -1870,7 +1932,7 @@ logger('4'); } $i = $r[0]; - + if($i['target']) $i['target'] = json_decode($i['target'],true); if($i['object']) @@ -1908,13 +1970,13 @@ logger('4'); * * @see item_store_update() * - * @param array $sender + * @param string $sender * @param array $item * @param array $orig * @param int $uid * @param boolean $tag_delivery + * @return void|array */ - static function update_imported_item($sender, $item, $orig, $uid, $tag_delivery) { // If this is a comment being updated, remove any privacy information @@ -1959,7 +2021,7 @@ logger('4'); /** * @brief Deletes an imported item. * - * @param array $sender + * @param string $sender * * \e string \b hash a xchan_hash * @param array $item * @param int $uid @@ -1977,9 +2039,9 @@ logger('4'); $r = q("select id, author_xchan, owner_xchan, source_xchan, item_deleted from item where ( author_xchan = '%s' or owner_xchan = '%s' or source_xchan = '%s' ) and mid = '%s' and uid = %d limit 1", - dbesc($sender['hash']), - dbesc($sender['hash']), - dbesc($sender['hash']), + dbesc($sender), + dbesc($sender), + dbesc($sender), dbesc($item['mid']), intval($uid) ); @@ -2054,7 +2116,7 @@ logger('4'); } foreach($deliveries as $d) { - + $DR = new DReport(z_root(),$sender,$d,$arr['mid']); $r = q("select * from channel where channel_portable_id = '%s' limit 1", @@ -2073,7 +2135,7 @@ logger('4'); 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. @@ -2133,18 +2195,18 @@ logger('4'); * @brief Processes delivery of profile. * * @see import_directory_profile() - * @param array $sender an associative array - * * \e string \b hash a xchan_hash + * + * @param string $sender * @param array $arr * @param array $deliveries (unused) + * @return void */ - static function process_profile_delivery($sender, $arr, $deliveries) { logger('process_profile_delivery', LOGGER_DEBUG); $r = q("select xchan_addr from xchan where xchan_hash = '%s' limit 1", - dbesc($sender['hash']) + dbesc($sender) ); if($r) { Libzotdir::import_directory_profile($sender, $arr, $r[0]['xchan_addr'], UPDATE_FLAGS_UPDATED, 0); @@ -2155,10 +2217,10 @@ logger('4'); /** * @brief * - * @param array $sender an associative array - * * \e string \b hash a xchan_hash + * @param string $sender * @param array $arr * @param array $deliveries (unused) deliveries is irrelevant + * @return void */ static function process_location_delivery($sender, $arr, $deliveries) { @@ -2176,7 +2238,7 @@ logger('4'); $x = Libsync::sync_locations($xchan,$arr,true); logger('results: ' . print_r($x,true), LOGGER_DEBUG); if($x['changed']) { - $guid = random_string() . '@' . App::get_hostname(); + //$guid = random_string() . '@' . App::get_hostname(); Libzotdir::update_modtime($sender,$r[0]['xchan_guid'],$arr['locations'][0]['address'],UPDATE_FLAGS_UPDATED); } } @@ -2200,8 +2262,8 @@ logger('4'); * * @param string $sender_hash A channel hash * @param array $locations + * @return void */ - static function check_location_move($sender_hash, $locations) { if(! $locations) @@ -2243,7 +2305,6 @@ logger('4'); } - /** * @brief Returns an array with all known distinct hubs for this channel. * @@ -2252,7 +2313,6 @@ logger('4'); * * \e string \b channel_hash the hash of the channel * @return array an array with associative arrays */ - static function encode_locations($channel) { $ret = []; @@ -2293,7 +2353,7 @@ logger('4'); if(! $z['site_id']) { $z['site_id'] = Libzot::make_xchan_hash($z['url'],$z['sitekey']); } - + $ret[] = $z; } } @@ -2306,10 +2366,8 @@ logger('4'); * @brief * * @param array $arr - * @param string $pubkey * @return boolean true if updated or inserted */ - static function import_site($arr) { if( (! is_array($arr)) || (! $arr['url']) || (! $arr['site_sig'])) @@ -2584,20 +2642,20 @@ logger('4'); $feed = ((x($arr,'feed')) ? intval($arr['feed']) : 0); if($ztarget) { - $t = q("select * from hubloc where hubloc_id_url = '%s' limit 1", + $t = q("select * from hubloc where hubloc_id_url = '%s' and hubloc_network = 'zot6' limit 1", dbesc($ztarget) ); if($t) { - + $ztarget_hash = $t[0]['hubloc_hash']; } else { - + // should probably perform discovery of the requestor (target) but if they actually had - // permissions we would know about them and we only want to know who they are to + // permissions we would know about them and we only want to know who they are to // enumerate their specific permissions - + $ztarget_hash = EMPTY_STR; } } @@ -2744,7 +2802,7 @@ logger('4'); $ret['id'] = $e['xchan_guid']; $ret['id_sig'] = self::sign($e['xchan_guid'], $e['channel_prvkey']); - $ret['primary_location'] = [ + $ret['primary_location'] = [ 'address' => $e['xchan_addr'], 'url' => $e['xchan_url'], 'connections_url' => $e['xchan_connurl'], @@ -2766,7 +2824,7 @@ logger('4'); $ret['searchable'] = $searchable; $ret['adult_content'] = $adult_channel; $ret['public_forum'] = $public_forum; - + $ret['comments'] = map_scope(PermissionLimits::Get($e['channel_id'],'post_comments')); $ret['mail'] = map_scope(PermissionLimits::Get($e['channel_id'],'post_mail')); @@ -2824,14 +2882,20 @@ logger('4'); $ret['locations'] = $x; $ret['site'] = self::site_info(); + /** + * @hooks zotinfo + * Hook to manipulate the zotinfo array before it is returned. + */ + call_hooks('zotinfo', $ret); - call_hooks('zotinfo',$ret); - - return($ret); - + return $ret; } - + /** + * @brief Get siteinfo. + * + * @return array + */ static function site_info() { $signing_key = get_config('system','prvkey'); @@ -2868,7 +2932,7 @@ logger('4'); if($dirmode != DIRECTORY_MODE_STANDALONE) { $register_policy = intval(get_config('system','register_policy')); - + if($register_policy == REGISTER_CLOSED) $ret['site']['register_policy'] = 'closed'; if($register_policy == REGISTER_APPROVE) @@ -2915,18 +2979,16 @@ logger('4'); } return $ret['site']; - } /** * @brief * * @param array $hub - * @param string $sitekey (optional, default empty) + * @param string $site_id (optional, default empty) * * @return string hubloc_url */ - static function update_hub_connected($hub, $site_id = '') { if ($site_id) { @@ -2985,12 +3047,21 @@ logger('4'); return $hub['hubloc_url']; } - + /** + * @brief + * + * @param string $data + * @param string $key + * @param string $alg (optional) default 'sha256' + * @return string + */ static function sign($data,$key,$alg = 'sha256') { if(! $key) return 'no key'; + $sig = ''; openssl_sign($data,$sig,$key,$alg); + return $alg . '.' . base64url_encode($sig); } @@ -3003,25 +3074,50 @@ logger('4'); if ($key && count($x) === 2) { $alg = $x[0]; $signature = base64url_decode($x[1]); - + $verify = @openssl_verify($data,$signature,$key,$alg); if ($verify === (-1)) { while ($msg = openssl_error_string()) { logger('openssl_verify: ' . $msg,LOGGER_NORMAL,LOG_ERR); } - btlogger('openssl_verify: key: ' . $key, LOGGER_DEBUG, LOG_ERR); + btlogger('openssl_verify: key: ' . $key, LOGGER_DEBUG, LOG_ERR); } } return(($verify > 0) ? true : false); } - - + /** + * @brief + * + * @return boolean + */ static function is_zot_request() { - $x = getBestSupportedMimeType([ 'application/x-zot+json' ]); + return(($x) ? true : false); } + + static public function zot_record_preferred($arr, $check = 'hubloc_network') { + + if(! $arr) { + return $arr; + } + + foreach($arr as $v) { + if($v[$check] === 'zot6') { + return $v; + } + } + foreach($arr as $v) { + if($v[$check] === 'zot') { + return $v; + } + } + + return $arr[0]; + + } + } diff --git a/Zotlabs/Lib/Libzotdir.php b/Zotlabs/Lib/Libzotdir.php index 91d089c86..1cb52275c 100644 --- a/Zotlabs/Lib/Libzotdir.php +++ b/Zotlabs/Lib/Libzotdir.php @@ -307,7 +307,7 @@ class Libzotdir { if ($ud['ud_addr'] && (! ($ud['ud_flags'] & UPDATE_FLAGS_DELETED))) { $success = false; - $href = \Zotlabs\Lib\Webfinger::zot_url(punify($url)); + $href = \Zotlabs\Lib\Webfinger::zot_url(punify($ud['ud_addr'])); if($href) { $zf = \Zotlabs\Lib\Zotfinger::exec($href); } @@ -651,4 +651,4 @@ class Libzotdir { -}
\ No newline at end of file +} diff --git a/Zotlabs/Lib/MessageFilter.php b/Zotlabs/Lib/MessageFilter.php index eb0fc3d2c..750d6d424 100644 --- a/Zotlabs/Lib/MessageFilter.php +++ b/Zotlabs/Lib/MessageFilter.php @@ -19,7 +19,7 @@ class MessageFilter { $lang = null; - if((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); } @@ -39,10 +39,17 @@ class MessageFilter { 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) === '*'))) + 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)) return false; + elseif((strpos($word,'lang!=') === 0) && ($lang) && (strcasecmp($lang,trim(substr($word,6))) != 0)) + return false; elseif(stristr($text,$word) !== false) return false; } @@ -60,10 +67,17 @@ class MessageFilter { 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) === '*'))) + 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)) return true; + elseif((strpos($word,'lang!=') === 0) && ($lang) && (strcasecmp($lang,trim(substr($word,6))) != 0)) + return true; elseif(stristr($text,$word) !== false) return true; } diff --git a/Zotlabs/Lib/PConfig.php b/Zotlabs/Lib/PConfig.php index 5e5954c95..c08c11e75 100644 --- a/Zotlabs/Lib/PConfig.php +++ b/Zotlabs/Lib/PConfig.php @@ -112,9 +112,11 @@ class PConfig { * The configuration key to set * @param string $value * The value to store + * @param string $updated (optional) + * The datetime to store * @return mixed Stored $value or false */ - static public function Set($uid, $family, $key, $value, $updated=NULL) { + static public function Set($uid, $family, $key, $value, $updated = NULL) { // this catches subtle errors where this function has been called // with local_channel() when not logged in (which returns false) @@ -131,14 +133,19 @@ class PConfig { $dbvalue = ((is_array($value)) ? serialize($value) : $value); $dbvalue = ((is_bool($dbvalue)) ? intval($dbvalue) : $dbvalue); + $now = datetime_convert(); if (! $updated) { - $updated = datetime_convert(); + //Sometimes things happen fast... very fast. + //To make sure legitimate updates aren't rejected + //because not enough time has passed. We say our updates + //happened just a short time in the past rather than right now. + $updated = datetime_convert('UTC','UTC','-2 seconds'); } $hash = hash('sha256',$family.':'.$key); if (self::Get($uid, 'hz_delpconfig', $hash) !== false) { - if (self::Get($uid, 'hz_delpconfig', $hash) > $updated) { + 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 { @@ -173,7 +180,7 @@ class PConfig { } else { - $new = (\App::$config[$uid][$family]['pcfgud:'.$key] < $updated); + $new = (\App::$config[$uid][$family]['pcfgud:'.$key] < $now); if ($new) { @@ -234,16 +241,18 @@ class PConfig { * The category of the configuration value * @param string $key * The configuration key to delete - * @return mixed + * @param string $updated (optional) + * The datetime to store + * @return boolean */ static public function Delete($uid, $family, $key, $updated = NULL) { if(is_null($uid) || $uid === false) return false; - $updated = ($updated) ? $updated : datetime_convert(); - - $newer = (\App::$config[$uid][$family]['pcfgud:'.$key] < $updated); + $updated = ($updated) ? $updated : datetime_convert('UTC','UTC','-2 seconds'); + $now = datetime_convert(); + $newer = (\App::$config[$uid][$family]['pcfgud:'.$key] < $now); if (! $newer) { logger('Refusing to delete pconfig with outdated delete request.', LOGGER_NORMAL, LOG_ERR); @@ -266,22 +275,13 @@ class PConfig { dbesc($key) ); + // Synchronize delete with clones. + if ($family != 'hz_delpconfig') { $hash = hash('sha256',$family.':'.$key); set_pconfig($uid,'hz_delpconfig',$hash,$updated); } - // Synchronize delete with 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 ($new) { - \App::$config[$uid]['transient'][$family]['pcfgdel:'.$key] = $updated; - } - return $ret; } diff --git a/Zotlabs/Lib/Share.php b/Zotlabs/Lib/Share.php index d3ecbf7fa..3a2ab1783 100644 --- a/Zotlabs/Lib/Share.php +++ b/Zotlabs/Lib/Share.php @@ -54,6 +54,7 @@ class Share { if(! $this->item) return $obj; + $obj['asld'] = $this->item['mid']; $obj['type'] = $this->item['obj_type']; $obj['id'] = $this->item['mid']; $obj['content'] = $this->item['body']; diff --git a/Zotlabs/Lib/ThreadItem.php b/Zotlabs/Lib/ThreadItem.php index 83d243177..9161aa182 100644 --- a/Zotlabs/Lib/ThreadItem.php +++ b/Zotlabs/Lib/ThreadItem.php @@ -38,6 +38,7 @@ class ThreadItem { $this->data = $data; $this->toplevel = ($this->get_id() == $this->get_data_value('parent')); + $this->threaded = get_config('system','thread_allow'); $observer = \App::get_observer(); @@ -76,7 +77,7 @@ class ThreadItem { * _ false on failure */ - public function get_template_data($conv_responses, $thread_level=1) { + public function get_template_data($conv_responses, $thread_level=1, $conv_flags = []) { $result = array(); @@ -101,6 +102,7 @@ class ThreadItem { || strlen($item['deny_cid']) || strlen($item['deny_gid'])))) ? t('Private Message') : false); + $shareable = ((($conv->get_profile_owner() == local_channel() && local_channel()) && ($item['item_private'] != 1)) ? true : false); // allow an exemption for sharing stuff from your private feeds @@ -115,6 +117,19 @@ class ThreadItem { $privacy_warning = true; } + if ($lock) { + if (($item['mid'] == $item['parent_mid']) && count(get_terms_oftype($item['term'],TERM_FORUM))) { + $privacy_warning = true; + $conv_flags['parent_privacy_warning'] = true; + } + } + + $privacy_warning = (isset($conv_flags['parent_privacy_warning'])) ? $conv_flags['parent_privacy_warning'] : $privacy_warning; + + if ($lock && $privacy_warning) { + $lock = t('Privacy conflict. Discretion advised.'); + } + $mode = $conv->get_mode(); switch($item['item_type']) { @@ -291,10 +306,18 @@ class ThreadItem { if($this->is_commentable() && $observer) { $like = array( t("I like this \x28toggle\x29"), t("like")); $dislike = array( t("I don't like this \x28toggle\x29"), t("dislike")); + $reply_to = array( t("Reply on this comment"), t("reply"), t("Reply to")); } - if ($shareable) - $share = array( t('Share This'), t('share')); + if ($shareable) { + // This actually turns out not to be possible in some protocol stacks without opening up hundreds of new issues. + // Will allow it only for uri resolvable sources. + if(strpos($item['mid'],'http') === 0) { + $share = []; //Not yet ready for primetime + //$share = array( t('Repeat This'), t('repeat')); + } + $embed = array( t('Share This'), t('share')); + } $dreport = ''; @@ -310,7 +333,6 @@ class ThreadItem { if(strcmp(datetime_convert('UTC','UTC',$item['created']),datetime_convert('UTC','UTC','now - 12 hours')) > 0) $is_new = true; - localize_item($item); $body = prepare_body($item,true); @@ -326,10 +348,6 @@ class ThreadItem { $comment_count_txt = sprintf( tt('%d comment','%d comments',$total_children),$total_children ); $list_unseen_txt = (($unseen_comments) ? sprintf('%d unseen',$unseen_comments) : ''); - - - - $children = $this->get_children(); $has_tags = (($body['tags'] || $body['categories'] || $body['mentions'] || $body['attachments'] || $body['folders']) ? true : false); @@ -352,13 +370,15 @@ class ThreadItem { 'text' => strip_tags($body['html']), 'id' => $this->get_id(), 'mid' => $item['mid'], + 'parent' => $item['parent'], + 'author_id' => (($item['author']['xchan_addr']) ? $item['author']['xchan_addr'] : $item['author']['xchan_url']), 'isevent' => $isevent, 'attend' => $attend, 'consensus' => $consensus, 'conlabels' => $conlabels, 'canvote' => $canvote, - 'linktitle' => sprintf( t('View %s\'s profile - %s'), $profile_name, $item['author']['xchan_addr']), - 'olinktitle' => sprintf( t('View %s\'s profile - %s'), $this->get_owner_name(), $item['owner']['xchan_addr']), + 'linktitle' => sprintf( t('View %s\'s profile - %s'), $profile_name, (($item['author']['xchan_addr']) ? $item['author']['xchan_addr'] : $item['author']['xchan_url'])), + 'olinktitle' => sprintf( t('View %s\'s profile - %s'), $this->get_owner_name(), (($item['owner']['xchan_addr']) ? $item['owner']['xchan_addr'] : $item['owner']['xchan_url'])), 'llink' => $item['llink'], 'viewthread' => $viewthread, 'to' => t('to'), @@ -404,23 +424,25 @@ class ThreadItem { 'has_tags' => $has_tags, 'reactions' => $this->reactions, // Item toolbar buttons - 'emojis' => (($this->is_toplevel() && $this->is_commentable() && $observer && feature_enabled($conv->get_profile_owner(),'emojis')) ? '1' : ''), + 'emojis' => (($this->is_toplevel() && $this->is_commentable() && $observer && feature_enabled($conv->get_profile_owner(),'emojis')) ? '1' : ''), 'like' => $like, 'dislike' => ((feature_enabled($conv->get_profile_owner(),'dislike')) ? $dislike : ''), + 'reply_to' => (((! $this->is_toplevel()) && feature_enabled($conv->get_profile_owner(),'reply_to')) ? $reply_to : ''), + 'top_hint' => t("Go to previous comment"), 'share' => $share, + 'embed' => $embed, 'rawmid' => $item['mid'], 'plink' => get_plink($item), 'edpost' => $edpost, // ((feature_enabled($conv->get_profile_owner(),'edit_posts')) ? $edpost : ''), - 'star' => ((feature_enabled($conv->get_profile_owner(),'star_posts')) ? $star : ''), + 'star' => ((feature_enabled($conv->get_profile_owner(),'star_posts') && ($item['item_type'] == ITEM_TYPE_POST)) ? $star : ''), 'tagger' => ((feature_enabled($conv->get_profile_owner(),'commtag')) ? $tagger : ''), - 'filer' => ((feature_enabled($conv->get_profile_owner(),'filing')) ? $filer : ''), + 'filer' => ((feature_enabled($conv->get_profile_owner(),'filing') && ($item['item_type'] == ITEM_TYPE_POST)) ? $filer : ''), 'bookmark' => (($conv->get_profile_owner() == local_channel() && local_channel() && $has_bookmarks) ? t('Save Bookmarks') : ''), 'addtocal' => (($has_event) ? t('Add to Calendar') : ''), 'drop' => $drop, 'multidrop' => ((feature_enabled($conv->get_profile_owner(),'multi_delete')) ? $multidrop : ''), - 'dropdown_extras' => $dropdown_extras, + 'dropdown_extras' => $dropdown_extras, // end toolbar buttons - 'unseen_comments' => $unseen_comments, 'comment_count' => $total_children, 'comment_count_txt' => $comment_count_txt, @@ -447,7 +469,8 @@ class ThreadItem { 'wait' => t('Please wait'), 'submid' => str_replace(['+','='], ['',''], base64_encode($item['mid'])), 'thread_level' => $thread_level, - 'settings' => $settings + 'settings' => $settings, + 'thr_parent' => (($item['parent_mid'] != $item['thr_parent']) ? $item['thr_parent'] : '') ); $arr = array('item' => $item, 'output' => $tmp_item); @@ -470,7 +493,7 @@ class ThreadItem { if(($this->get_display_mode() === 'normal') && ($nb_children > 0)) { foreach($children as $child) { - $result['children'][] = $child->get_template_data($conv_responses, $thread_level + 1); + $result['children'][] = $child->get_template_data($conv_responses, $thread_level + 1,$conv_flags); } // Collapse if(($nb_children > $visible_comments) || ($thread_level > 1)) { @@ -792,7 +815,7 @@ class ThreadItem { '$anonname' => [ 'anonname', t('Your full name (required)') ], '$anonmail' => [ 'anonmail', t('Your email address (required)') ], '$anonurl' => [ 'anonurl', t('Your website URL (optional)') ], - '$auto_save_draft' => $feature_auto_save_draft, + '$auto_save_draft' => $feature_auto_save_draft )); return $comment_box; @@ -847,4 +870,3 @@ class ThreadItem { } - diff --git a/Zotlabs/Lib/ThreadListener.php b/Zotlabs/Lib/ThreadListener.php new file mode 100644 index 000000000..308e02255 --- /dev/null +++ b/Zotlabs/Lib/ThreadListener.php @@ -0,0 +1,53 @@ +<?php + +namespace Zotlabs\Lib; + +class ThreadListener { + + static public function store($target_id,$portable_id,$ltype = 0) { + $x = self::fetch($target_id,$portable_id,$ltype = 0); + if(! $x) { + $r = q("insert into listeners ( target_id, portable_id, ltype ) values ( '%s', '%s' , %d ) ", + dbesc($target_id), + dbesc($portable_id), + intval($ltype) + ); + } + } + + static public function fetch($target_id,$portable_id,$ltype = 0) { + $x = q("select * from listeners where target_id = '%s' and portable_id = '%s' and ltype = %d limit 1", + dbesc($target_id), + dbesc($portable_id), + intval($ltype) + ); + if($x) { + return $x[0]; + } + return false; + } + + static public function fetch_by_target($target_id,$ltype = 0) { + $x = q("select * from listeners where target_id = '%s' and ltype = %d", + dbesc($target_id), + intval($ltype) + ); + + return $x; + } + + static public function delete_by_target($target_id, $ltype = 0) { + return q("delete from listeners where target_id = '%s' and ltype = %d", + dbesc($target_id), + intval($ltype) + ); + } + + static public function delete_by_pid($portable_id, $ltype = 0) { + return q("delete from listeners where portable_id = '%s' and ltype = %d", + dbesc($portable_id), + intval($ltype) + ); + } + +} diff --git a/Zotlabs/Lib/ZotURL.php b/Zotlabs/Lib/ZotURL.php new file mode 100644 index 000000000..bc14c516a --- /dev/null +++ b/Zotlabs/Lib/ZotURL.php @@ -0,0 +1,91 @@ +<?php + +namespace Zotlabs\Lib; + +use Zotlabs\Zot6\HTTPSig; + + +class ZotURL { + + static public function fetch($url,$channel) { + + $ret = [ 'success' => false ]; + + if(strpos($url,'x-zot:') !== 0) { + return $ret; + } + + + if(! $url) { + return $ret; + } + + $portable_url = substr($url,6); + $u = explode('/',$portable_url); + $portable_id = $u[0]; + + $hosts = self::lookup($portable_id); + + if(! $hosts) { + return $ret; + } + + foreach($hosts as $h) { + $newurl = $h . '/id/' . $portable_url; + + $m = parse_url($newurl); + + $data = json_encode([ 'zot_token' => random_string() ]); + + if($channel && $m) { + + $headers = [ + 'Accept' => 'application/x-zot+json', + 'Content-Type' => 'application/x-zot+json', + 'X-Zot-Token' => random_string(), + 'Digest' => HTTPSig::generate_digest_header($data), + 'Host' => $m['host'], + '(request-target)' => 'post ' . get_request_string($newurl) + ]; + $h = HTTPSig::create_sig($headers,$channel['channel_prvkey'],channel_url($channel),false); + } + else { + $h = [ 'Accept: application/x-zot+json' ]; + } + + $result = []; + + $redirects = 0; + $x = z_post_url($newurl,$data,$redirects, [ 'headers' => $h ] ); + if($x['success']) { + return $x; + } + } + + return $ret; + + } + + static public function is_zoturl($url) { + + if(strpos($url,'x-zot:') === 0) { + return true; + } + return false; + } + + + static public function lookup($portable_id) { + + $r = q("select * from hubloc left join site on hubloc_url = site_url where hubloc_hash = '%s' and site_dead = 0 order by hubloc_primary desc", + dbesc($portable_id) + ); + + if(! $r) { + // extend to network lookup + return false; + } + return ids_to_array($r,'hubloc_url'); + } + +}
\ No newline at end of file |