diff options
-rw-r--r-- | Zotlabs/Access/Permissions.php | 1 | ||||
-rw-r--r-- | Zotlabs/Daemon/Cron.php | 2 | ||||
-rw-r--r-- | Zotlabs/Daemon/Cron_weekly.php | 15 | ||||
-rw-r--r-- | Zotlabs/Module/Acl.php | 11 | ||||
-rw-r--r-- | Zotlabs/Module/Directory.php | 5 | ||||
-rw-r--r-- | Zotlabs/Module/Item.php | 9 | ||||
-rw-r--r-- | Zotlabs/Module/Settings/Channel.php | 4 | ||||
-rw-r--r-- | Zotlabs/Widget/Conversations.php | 12 | ||||
-rw-r--r-- | include/attach.php | 2 | ||||
-rw-r--r-- | include/channel.php | 60 | ||||
-rw-r--r-- | include/conversation.php | 22 | ||||
-rw-r--r-- | include/group.php | 8 | ||||
-rwxr-xr-x | include/items.php | 304 | ||||
-rw-r--r-- | include/message.php | 5 | ||||
-rw-r--r-- | tests/unit/Access/PermissionLimitsTest.php | 78 | ||||
-rw-r--r-- | tests/unit/Access/PermissionRolesTest.php | 100 | ||||
-rw-r--r-- | tests/unit/Access/PermissionsTest.php | 244 | ||||
-rw-r--r-- | view/theme/redbasic/js/redbasic.js | 14 | ||||
-rw-r--r-- | view/tpl/notifications_widget.tpl | 16 | ||||
-rwxr-xr-x | view/tpl/photo_view.tpl | 2 | ||||
-rwxr-xr-x | view/tpl/settings.tpl | 3 |
21 files changed, 656 insertions, 261 deletions
diff --git a/Zotlabs/Access/Permissions.php b/Zotlabs/Access/Permissions.php index 20ce21238..bca40a9c1 100644 --- a/Zotlabs/Access/Permissions.php +++ b/Zotlabs/Access/Permissions.php @@ -127,6 +127,7 @@ class Permissions { static public function FilledPerms($arr) { if(is_null($arr)) { btlogger('FilledPerms: null'); + $arr = []; } $everything = self::Perms(); diff --git a/Zotlabs/Daemon/Cron.php b/Zotlabs/Daemon/Cron.php index 65edbedfa..01c43262a 100644 --- a/Zotlabs/Daemon/Cron.php +++ b/Zotlabs/Daemon/Cron.php @@ -78,7 +78,7 @@ class Cron { // channels and sites that quietly vanished and prevent the directory from accumulating stale // or dead entries. - $r = q("select channel_id from channel where channel_dirdate < %s - INTERVAL %s", + $r = q("select channel_id from channel where channel_dirdate < %s - INTERVAL %s and channel_removed = 0", db_utcnow(), db_quoteinterval('30 DAY') ); diff --git a/Zotlabs/Daemon/Cron_weekly.php b/Zotlabs/Daemon/Cron_weekly.php index 5b185f475..d44400767 100644 --- a/Zotlabs/Daemon/Cron_weekly.php +++ b/Zotlabs/Daemon/Cron_weekly.php @@ -21,6 +21,21 @@ class Cron_weekly { mark_orphan_hubsxchans(); + // Find channels that were removed in the last three weeks, but + // haven't been finally cleaned up. These should be older than 10 + // days to ensure that "purgeall" messages have gone out or bounced + // or timed out. + + $r = q("select channel_id from channel where channel_removed = 1 and + channel_deleted > %s - INTERVAL %s and channel_deleted < %s - INTERVAL %s", + db_utcnow(), db_quoteinterval('21 DAY'), + db_utcnow(), db_quoteinterval('10 DAY') + ); + if($r) { + foreach($r as $rv) { + channel_remove_final($rv['channel_id']); + } + } // get rid of really old poco records diff --git a/Zotlabs/Module/Acl.php b/Zotlabs/Module/Acl.php index e164875e8..ad1c8b8cd 100644 --- a/Zotlabs/Module/Acl.php +++ b/Zotlabs/Module/Acl.php @@ -176,11 +176,18 @@ class Acl extends \Zotlabs\Web\Controller { $extra_channels_sql = " OR (abook_channel IN ($extra_channels_sql)) and abook_hidden = 0 "; - // Add atokens belonging to the local channel @TODO restrict by search + // Add atokens belonging to the local channel + + if($search) { + $sql_extra_atoken = "AND ( atoken_name LIKE " . protect_sprintf( "'%" . dbesc($search) . "%'" ) . ") "; + } + else { + $sql_extra_atoken = ''; + } $r2 = null; - $r1 = q("select * from atoken where atoken_uid = %d", + $r1 = q("select * from atoken where atoken_uid = %d $sql_extra_atoken", intval(local_channel()) ); diff --git a/Zotlabs/Module/Directory.php b/Zotlabs/Module/Directory.php index 256667ef3..b1552a694 100644 --- a/Zotlabs/Module/Directory.php +++ b/Zotlabs/Module/Directory.php @@ -64,6 +64,11 @@ class Directory extends \Zotlabs\Web\Controller { return; } + if(get_config('system','block_public_directory',false) && (! get_observer_hash())) { + notice( t('Public access denied.') . EOL); + return; + } + $observer = get_observer_hash(); $globaldir = get_directory_setting($observer, 'globaldir'); diff --git a/Zotlabs/Module/Item.php b/Zotlabs/Module/Item.php index b54de0fb9..f2b850ffc 100644 --- a/Zotlabs/Module/Item.php +++ b/Zotlabs/Module/Item.php @@ -577,15 +577,6 @@ class Item extends \Zotlabs\Web\Controller { * so we'll set the permissions regardless and realise that the media may not be * referenced in the post. * - * What is preventing us from being able to upload photos into comments is dealing with - * the photo and attachment permissions, since we don't always know who was in the - * distribution for the top level post. - * - * We might be able to provide this functionality with a lot of fiddling: - * - if the top level post is public (make the photo public) - * - if the top level post was written by us or a wall post that belongs to us (match the top level post) - * - if the top level post has privacy mentions, add those to the permissions. - * - otherwise disallow the photo *or* make the photo public. This is the part that gets messy. */ if(! $preview) { diff --git a/Zotlabs/Module/Settings/Channel.php b/Zotlabs/Module/Settings/Channel.php index 41e23b717..63370a141 100644 --- a/Zotlabs/Module/Settings/Channel.php +++ b/Zotlabs/Module/Settings/Channel.php @@ -148,6 +148,8 @@ class Channel { $defpermcat = ((x($_POST,'defpermcat')) ? notags(trim($_POST['defpermcat'])) : 'default'); $cal_first_day = (((x($_POST,'first_day')) && (intval($_POST['first_day']) == 1)) ? 1: 0); + $mailhost = ((array_key_exists('mailhost',$_POST)) ? notags(trim($_POST['mailhost'])) : ''); + $pageflags = $channel['channel_pageflags']; $existing_adult = (($pageflags & PAGE_ADULT) ? 1 : 0); @@ -239,6 +241,7 @@ class Channel { set_pconfig(local_channel(),'system','attach_path',$attach_path); set_pconfig(local_channel(),'system','cal_first_day',$cal_first_day); set_pconfig(local_channel(),'system','default_permcat',$defpermcat); + set_pconfig(local_channel(),'system','email_notify_host',$mailhost); $r = q("update channel set channel_name = '%s', channel_pageflags = %d, channel_timezone = '%s', channel_location = '%s', channel_notifyflags = %d, channel_max_anon_mail = %d, channel_max_friend_req = %d, channel_expire_days = %d $set_perms where channel_id = %d", dbesc($username), @@ -561,6 +564,7 @@ class Channel { '$vnotify11' => array('vnotify11', t('System Registrations'), ($vnotify & VNOTIFY_REGISTER), VNOTIFY_REGISTER, '', $yes_no), '$vnotify12' => array('vnotify12', t('Unseen shared files'), ($vnotify & VNOTIFY_FILES), VNOTIFY_FILES, '', $yes_no), '$vnotify13' => ((get_config('system', 'disable_discover_tab') != 1) ? array('vnotify13', t('Unseen public activity'), ($vnotify & VNOTIFY_PUBS), VNOTIFY_PUBS, '', $yes_no) : array()), + '$mailhost' => [ 'mailhost', t('Email notification hub (hostname)'), get_pconfig(local_channel(),'system','email_notify_host',\App::get_hostname()), sprintf( t('If your channel is mirrored to multiple hubs, set this to your preferred location. This will prevent duplicate email notifications. Example: %s'),\App::get_hostname()) ], '$always_show_in_notices' => array('always_show_in_notices', t('Also show new wall posts, private messages and connections under Notices'), $always_show_in_notices, 1, '', $yes_no), '$evdays' => array('evdays', t('Notify me of events this many days in advance'), $evdays, t('Must be greater than 0')), diff --git a/Zotlabs/Widget/Conversations.php b/Zotlabs/Widget/Conversations.php index 56510750f..267d50fa0 100644 --- a/Zotlabs/Widget/Conversations.php +++ b/Zotlabs/Widget/Conversations.php @@ -28,6 +28,8 @@ class Conversations { require_once('include/message.php'); + $o = ''; + // private_messages_list() can do other more complicated stuff, for now keep it simple $r = private_messages_list(local_channel(), $mailbox, \App::$pager['start'], \App::$pager['itemspage']); @@ -36,13 +38,13 @@ class Conversations { return $o; } - $messages = array(); + $messages = []; foreach($r as $rr) { $selected = ((argc() == 3) ? intval(argv(2)) == intval($rr['id']) : $r[0]['id'] == $rr['id']); - $messages[] = array( + $messages[] = [ 'mailbox' => $mailbox, 'id' => $rr['id'], 'from_name' => $rr['from']['xchan_name'], @@ -57,14 +59,14 @@ class Conversations { 'date' => datetime_convert('UTC',date_default_timezone_get(),$rr['created'], 'c'), 'seen' => $rr['seen'], 'selected' => ((argv(1) != 'new') ? $selected : '') - ); + ]; } $tpl = get_markup_template('mail_head.tpl'); - $o .= replace_macros($tpl, array( + $o .= replace_macros($tpl, [ '$header' => $header, '$messages' => $messages - )); + ]); } return $o; diff --git a/include/attach.php b/include/attach.php index 179a57a90..2bb57722b 100644 --- a/include/attach.php +++ b/include/attach.php @@ -1366,7 +1366,7 @@ function attach_delete($channel_id, $resource, $is_photo = 0) { return; } - $url = get_cloudpath($channel_id, $channel_address, $resource); + $url = get_cloud_url($channel_id, $channel_address, $resource); $object = get_file_activity_object($channel_id, $resource, $url); // If resource is a directory delete everything in the directory recursive diff --git a/include/channel.php b/include/channel.php index 6a6022aba..4f0e8ec6a 100644 --- a/include/channel.php +++ b/include/channel.php @@ -2527,19 +2527,43 @@ function channel_remove($channel_id, $local = true, $unset_session = false) { } } + q("DELETE FROM app WHERE app_channel = %d", intval($channel_id)); + q("DELETE FROM atoken WHERE atoken_uid = %d", intval($channel_id)); + q("DELETE FROM chatroom WHERE cr_uid = %d", intval($channel_id)); + q("DELETE FROM conv WHERE uid = %d", intval($channel_id)); q("DELETE FROM groups WHERE uid = %d", intval($channel_id)); q("DELETE FROM group_member WHERE uid = %d", intval($channel_id)); q("DELETE FROM event WHERE uid = %d", intval($channel_id)); - q("DELETE FROM item WHERE uid = %d", intval($channel_id)); q("DELETE FROM mail WHERE channel_id = %d", intval($channel_id)); + q("DELETE FROM menu WHERE menu_channel_id = %d", intval($channel_id)); + q("DELETE FROM menu_item WHERE mitem_channel_id = %d", intval($channel_id)); + q("DELETE FROM notify WHERE uid = %d", intval($channel_id)); + q("DELETE FROM obj WHERE obj_channel = %d", intval($channel_id)); + + q("DELETE FROM photo WHERE uid = %d", intval($channel_id)); q("DELETE FROM attach WHERE uid = %d", intval($channel_id)); q("DELETE FROM profile WHERE uid = %d", intval($channel_id)); - q("DELETE FROM pconfig WHERE uid = %d", intval($channel_id)); + q("DELETE FROM src WHERE src_channel_id = %d", intval($channel_id)); + + $r = q("select resource_id FROM attach WHERE uid = %d", intval($channel_id)); + if($r) { + foreach($r as $rv) { + attach_delete($channel_id,$rv['resource_id']); + } + } + + + + $r = q("select id from item where uid = %d", intval($channel_id)); + if($r) { + foreach($r as $rv) { + drop_item($rv['id'],false); + } + } - /// @FIXME At this stage we need to remove the file resources located under /store/$nickname q("delete from abook where abook_xchan = '%s' and abook_self = 1 ", dbesc($channel['channel_hash']) @@ -2593,19 +2617,11 @@ function channel_remove($channel_id, $local = true, $unset_session = false) { } //remove from file system - $r = q("select channel_address from channel where channel_id = %d limit 1", - intval($channel_id) - ); - if($r) { - $channel_address = $r[0]['channel_address'] ; - } - if($channel_address) { - $f = 'store/' . $channel_address.'/'; - logger('delete '. $f); - if(is_dir($f)) { - @rrmdir($f); - } + + $f = 'store/' . $channel['channel_address']; + if(is_dir($f)) { + @rrmdir($f); } Zotlabs\Daemon\Master::Summon(array('Directory',$channel_id)); @@ -2616,6 +2632,20 @@ function channel_remove($channel_id, $local = true, $unset_session = false) { } } +// execute this at least a week after removing a channel + +function channel_remove_final($channel_id) { + + q("delete from abook where abook_channel = %d", intval($channel_id)); + q("delete from abconfig where chan = %d", intval($channel_id)); + q("delete from pconfig where uid = %d", intval($channel_id)); + + +} + + + + /** * @brief This checks if a channel is allowed to publish executable code. * diff --git a/include/conversation.php b/include/conversation.php index 0b9df5acd..1cbd9116c 100644 --- a/include/conversation.php +++ b/include/conversation.php @@ -573,22 +573,16 @@ function conversation($items, $mode, $update, $page_mode = 'traditional', $prepa if (! feature_enabled($profile_owner,'multi_delete')) $page_dropping = false; - $uploading = true; - - if($profile_owner > 0) { - $owner_channel = channelx_by_n($profile_owner); - if($owner_channel['channel_allow_cid'] || $owner_channel['channel_allow_gid'] - || $owner_channel['channel_deny_cid'] || $owner_channel['channel_deny_gid']) { - $uploading = false; - } - if(\Zotlabs\Access\PermissionLimits::Get($profile_owner,'view_storage') !== PERMS_PUBLIC) { - $uploading = false; + $uploading = false; + + if(local_channel()) { + $cur_channel = App::get_channel(); + if($cur_channel['channel_allow_cid'] === '' && $cur_channel['channel_allow_gid'] === '' + && $cur_channel['channel_deny_cid'] === '' && $cur_channel['channel_deny_gid'] === '' + && intval(\Zotlabs\Access\PermissionLimits::Get(local_channel(),'view_storage')) === PERMS_PUBLIC) { + $uploading = true; } } - else { - $uploading = false; - } - $channel = App::get_channel(); $observer = App::get_observer(); diff --git a/include/group.php b/include/group.php index e0c20b536..03ebf7ee5 100644 --- a/include/group.php +++ b/include/group.php @@ -18,10 +18,6 @@ function group_add($uid,$name,$public = 0) { intval($r) ); if(($z) && $z[0]['deleted']) { - /*$r = q("UPDATE groups SET deleted = 0 WHERE uid = %d AND gname = '%s'", - intval($uid), - dbesc($name) - );*/ q('UPDATE groups SET deleted = 0 WHERE id = %d', intval($z[0]['id'])); notice( t('A deleted group with this name was revived. Existing item permissions <strong>may</strong> apply to this group and any future members. If this is not what you intended, please create another group with a different name.') . EOL); } @@ -81,11 +77,11 @@ function group_rmv($uid,$name) { $user_info['channel_default_group'] = ''; $change = true; } - if(strpos($user_info['channel_allow_gid'], '<' . $group_id . '>') !== false) { + if(strpos($user_info['channel_allow_gid'], '<' . $group_hash . '>') !== false) { $user_info['channel_allow_gid'] = str_replace('<' . $group_hash . '>', '', $user_info['channel_allow_gid']); $change = true; } - if(strpos($user_info['channel_deny_gid'], '<' . $group_id . '>') !== false) { + if(strpos($user_info['channel_deny_gid'], '<' . $group_hash . '>') !== false) { $user_info['channel_deny_gid'] = str_replace('<' . $group_hash . '>', '', $user_info['channel_deny_gid']); $change = true; } diff --git a/include/items.php b/include/items.php index 5d592c736..b1b40e977 100755 --- a/include/items.php +++ b/include/items.php @@ -2571,148 +2571,149 @@ function tag_deliver($uid, $item_id) { if($terms) logger('Post mentions: ' . print_r($terms,true), LOGGER_DATA); + + $max_forums = get_config('system','max_tagged_forums',2); + $matched_forums = 0; + + $link = normalise_link($u[0]['xchan_url']); + if($terms) { foreach($terms as $term) { - if(link_compare($term['url'],$link)) { - $mention = true; - break; + if(! link_compare($term['url'],$link)) { + continue; } - } - } - if($mention) { - logger('Mention found for ' . $u[0]['channel_name']); + $mention = true; - $r = q("update item set item_mentionsme = 1 where id = %d", - intval($item_id) - ); + logger('Mention found for ' . $u[0]['channel_name']); - // At this point we've determined that the person receiving this post was mentioned in it or it is a union. - // Now let's check if this mention was inside a reshare so we don't spam a forum - // If it's private we may have to unobscure it momentarily so that we can parse it. + $r = q("update item set item_mentionsme = 1 where id = %d", + intval($item_id) + ); - $body = $item['body']; + // At this point we've determined that the person receiving this post was mentioned in it or it is a union. + // Now let's check if this mention was inside a reshare so we don't spam a forum + // If it's private we may have to unobscure it momentarily so that we can parse it. - $body = preg_replace('/\[share(.*?)\[\/share\]/','',$body); + $body = preg_replace('/\[share(.*?)\[\/share\]/','',$item['body']); - $tagged = false; - $plustagged = false; - $matches = array(); + $tagged = false; + $plustagged = false; + $matches = array(); - $pattern = '/[\!@]\!?\[zrl\=' . preg_quote($term['url'],'/') . '\]' . preg_quote($term['term'],'/') . '\[\/zrl\]/'; - if(preg_match($pattern,$body,$matches)) - $tagged = true; + $pattern = '/[\!@]\!?\[zrl\=' . preg_quote($term['url'],'/') . '\]' . preg_quote($term['term'],'/') . '\[\/zrl\]/'; + if(preg_match($pattern,$body,$matches)) + $tagged = true; - // original red forum tagging sequence @forumname+ - // standard forum tagging sequence !forumname + // original red forum tagging sequence @forumname+ + // standard forum tagging sequence !forumname - $pluspattern = '/@\!?\[zrl\=([^\]]*?)\]((?:.(?!\[zrl\=))*?)\+\[\/zrl\]/'; + $pluspattern = '/@\!?\[zrl\=([^\]]*?)\]((?:.(?!\[zrl\=))*?)\+\[\/zrl\]/'; - $forumpattern = '/\!\!?\[zrl\=([^\]]*?)\]((?:.(?!\[zrl\=))*?)\[\/zrl\]/'; + $forumpattern = '/\!\!?\[zrl\=([^\]]*?)\]((?:.(?!\[zrl\=))*?)\[\/zrl\]/'; - $found = false; + $found = false; - $max_forums = get_config('system','max_tagged_forums'); - if(! $max_forums) - $max_forums = 2; - $matched_forums = 0; - $matches = array(); + $matches = array(); - if(preg_match_all($pluspattern,$body,$matches,PREG_SET_ORDER)) { - foreach($matches as $match) { - $matched_forums ++; - if($term['url'] === $match[1] && $term['term'] === $match[2]) { - if($matched_forums <= $max_forums) { - $plustagged = true; - $found = true; - break; + if(preg_match_all($pluspattern,$body,$matches,PREG_SET_ORDER)) { + foreach($matches as $match) { + $matched_forums ++; + if($term['url'] === $match[1] && $term['term'] === $match[2] && intval($term['ttype']) === TERM_MENTION) { + if($matched_forums <= $max_forums) { + $plustagged = true; + $found = true; + break; + } + logger('forum ' . $term['term'] . ' exceeded max_tagged_forums - ignoring'); } - logger('forum ' . $term['term'] . ' exceeded max_tagged_forums - ignoring'); } } - } - if(preg_match_all($forumpattern,$body,$matches,PREG_SET_ORDER)) { - foreach($matches as $match) { - $matched_forums ++; - if($term['url'] === $match[1] && $term['term'] === $match[2]) { - if($matched_forums <= $max_forums) { - $plustagged = true; - $found = true; - break; + if(preg_match_all($forumpattern,$body,$matches,PREG_SET_ORDER)) { + foreach($matches as $match) { + $matched_forums ++; + if($term['url'] === $match[1] && $term['term'] === $match[2] && intval($term['ttype']) === TERM_FORUM) { + if($matched_forums <= $max_forums) { + $plustagged = true; + $found = true; + break; + } + logger('forum ' . $term['term'] . ' exceeded max_tagged_forums - ignoring'); } - logger('forum ' . $term['term'] . ' exceeded max_tagged_forums - ignoring'); } } - } - if(! ($tagged || $plustagged)) { - logger('Mention was in a reshare or exceeded max_tagged_forums - ignoring'); - return; - } + if(! ($tagged || $plustagged)) { + logger('Mention was in a reshare or exceeded max_tagged_forums - ignoring'); + continue; + } - $arr = [ - 'channel_id' => $uid, - 'item' => $item, - 'body' => $body - ]; - /** - * @hooks tagged - * Called when a delivery is processed which results in you being tagged. - * * \e number \b channel_id - * * \e array \b item - * * \e string \b body - */ - call_hooks('tagged', $arr); + $arr = [ + 'channel_id' => $uid, + 'item' => $item, + 'body' => $body + ]; + /** + * @hooks tagged + * Called when a delivery is processed which results in you being tagged. + * * \e number \b channel_id + * * \e array \b item + * * \e string \b body + */ + call_hooks('tagged', $arr); + + /* + * Kill two birds with one stone. As long as we're here, send a mention notification. + */ - /* - * Kill two birds with one stone. As long as we're here, send a mention notification. - */ + Zlib\Enotify::submit(array( + 'to_xchan' => $u[0]['channel_hash'], + 'from_xchan' => $item['author_xchan'], + 'type' => NOTIFY_TAGSELF, + 'item' => $item, + 'link' => $i[0]['llink'], + 'verb' => ACTIVITY_TAG, + 'otype' => 'item' + )); - Zlib\Enotify::submit(array( - 'to_xchan' => $u[0]['channel_hash'], - 'from_xchan' => $item['author_xchan'], - 'type' => NOTIFY_TAGSELF, - 'item' => $item, - 'link' => $i[0]['llink'], - 'verb' => ACTIVITY_TAG, - 'otype' => 'item' - )); - - // Just a normal tag? - - if(! $plustagged) { - logger('Not a plus tag', LOGGER_DEBUG); - return; - } + // Just a normal tag? - // plustagged - keep going, next check permissions + if(! $plustagged) { + logger('Not a plus tag', LOGGER_DEBUG); + continue; + } - if(! perm_is_allowed($uid,$item['author_xchan'],'tag_deliver')) { - logger('tag_delivery denied for uid ' . $uid . ' and xchan ' . $item['author_xchan']); - return; - } - } + // plustagged - keep going, next check permissions + + if(! perm_is_allowed($uid,$item['author_xchan'],'tag_deliver')) { + logger('tag_delivery denied for uid ' . $uid . ' and xchan ' . $item['author_xchan']); + continue; + } - if((! $mention) && (! $union)) { - logger('No mention for ' . $u[0]['channel_name'] . ' and no union.'); - return; - } - // tgroup delivery - setup a second delivery chain - // prevent delivery looping - only proceed - // if the message originated elsewhere and is a top-level post + if((! $mention) && (! $union)) { + logger('No mention for ' . $u[0]['channel_name'] . ' and no union.'); + continue; + } + // tgroup delivery - setup a second delivery chain + // prevent delivery looping - only proceed + // if the message originated elsewhere and is a top-level post - if(intval($item['item_wall']) || intval($item['item_origin']) || (! intval($item['item_thread_top'])) || ($item['id'] != $item['parent'])) { - logger('Item was local or a comment. rejected.'); - return; - } - logger('Creating second delivery chain.'); - start_delivery_chain($u[0],$item,$item_id,null); + if(intval($item['item_wall']) || intval($item['item_origin']) || (! intval($item['item_thread_top'])) || ($item['id'] != $item['parent'])) { + logger('Item was local or a comment. rejected.'); + continue; + } + + logger('Creating second delivery chain.'); + start_delivery_chain($u[0],$item,$item_id,null); + + } + } } /** @@ -2760,78 +2761,73 @@ function tgroup_check($uid, $item) { if($terms) logger('tgroup_check: post mentions: ' . print_r($terms,true), LOGGER_DATA); + $max_forums = get_config('system','max_tagged_forums',2); + $matched_forums = 0; + $link = normalise_link($u[0]['xchan_url']); if($terms) { foreach($terms as $term) { - if(link_compare($term['url'],$link)) { - $mention = true; - break; + if(! link_compare($term['url'],$link)) { + continue; } - } - } - - if($mention) { - logger('tgroup_check: mention found for ' . $u[0]['channel_name']); - } - else - return false; - // At this point we've determined that the person receiving this post was mentioned in it. - // Now let's check if this mention was inside a reshare so we don't spam a forum - // note: $term has been set to the matching term + $mention = true; + logger('tgroup_check: mention found for ' . $u[0]['channel_name']); + // At this point we've determined that the person receiving this post was mentioned in it. + // Now let's check if this mention was inside a reshare so we don't spam a forum + // note: $term has been set to the matching term - $body = $item['body']; - $body = preg_replace('/\[share(.*?)\[\/share\]/','',$body); + $body = preg_replace('/\[share(.*?)\[\/share\]/','',$item['body']); - $pluspattern = '/@\!?\[zrl\=([^\]]*?)\]((?:.(?!\[zrl\=))*?)\+\[\/zrl\]/'; + $pluspattern = '/@\!?\[zrl\=([^\]]*?)\]((?:.(?!\[zrl\=))*?)\+\[\/zrl\]/'; - $forumpattern = '/\!\!?\[zrl\=([^\]]*?)\]((?:.(?!\[zrl\=))*?)\[\/zrl\]/'; + $forumpattern = '/\!\!?\[zrl\=([^\]]*?)\]((?:.(?!\[zrl\=))*?)\[\/zrl\]/'; + $found = false; - $found = false; + $matches = array(); - $max_forums = get_config('system','max_tagged_forums'); - if(! $max_forums) - $max_forums = 2; - $matched_forums = 0; - $matches = array(); - - if(preg_match_all($pluspattern,$body,$matches,PREG_SET_ORDER)) { - foreach($matches as $match) { - $matched_forums ++; - if($term['url'] === $match[1] && $term['term'] === $match[2]) { - if($matched_forums <= $max_forums) { - $found = true; - break; + if(preg_match_all($pluspattern,$body,$matches,PREG_SET_ORDER)) { + foreach($matches as $match) { + $matched_forums ++; + if($term['url'] === $match[1] && $term['term'] === $match[2] && intval($term['ttype']) === TERM_MENTION) { + if($matched_forums <= $max_forums) { + $found = true; + break; + } + logger('forum ' . $term['term'] . ' exceeded max_tagged_forums - ignoring'); + } } - logger('forum ' . $term['term'] . ' exceeded max_tagged_forums - ignoring'); } - } - } - if(preg_match_all($forumpattern,$body,$matches,PREG_SET_ORDER)) { - foreach($matches as $match) { - $matched_forums ++; - if($term['url'] === $match[1] && $term['term'] === $match[2]) { - if($matched_forums <= $max_forums) { - $found = true; - break; + if(preg_match_all($forumpattern,$body,$matches,PREG_SET_ORDER)) { + foreach($matches as $match) { + $matched_forums ++; + if($term['url'] === $match[1] && $term['term'] === $match[2] && intval($term['ttype']) === TERM_FORUM) { + if($matched_forums <= $max_forums) { + $found = true; + break; + } + logger('forum ' . $term['term'] . ' exceeded max_tagged_forums - ignoring'); + } } - logger('forum ' . $term['term'] . ' exceeded max_tagged_forums - ignoring'); } + + if(! $found) { + logger('tgroup_check: mention was in a reshare or exceeded max_tagged_forums - ignoring'); + continue; + } + + return true; } } - if(! $found) { - logger('tgroup_check: mention was in a reshare or exceeded max_tagged_forums - ignoring'); - return false; - } + return false; - return true; } /** diff --git a/include/message.php b/include/message.php index 477c7172c..b57d2e068 100644 --- a/include/message.php +++ b/include/message.php @@ -335,12 +335,9 @@ function private_messages_list($uid, $mailbox = '', $start = 0, $numitems = 0) { case 'combined': default: - - $parents = q("SELECT parent_mid FROM mail WHERE mid = parent_mid AND channel_id = %d ORDER BY created DESC", + $parents = q("SELECT mail.parent_mid FROM mail LEFT JOIN conv ON mail.conv_guid = conv.guid WHERE mail.mid = mail.parent_mid AND mail.channel_id = %d ORDER BY conv.updated DESC $limit", dbesc($local_channel) ); - //FIXME: We need the latest mail of a thread here. This query throws errors in postgres. We now look for the latest in php until somebody can fix this... - //$sql = "SELECT * FROM ( SELECT * FROM mail WHERE channel_id = $local_channel ORDER BY created DESC $limit ) AS temp_table GROUP BY parent_mid ORDER BY created DESC"; break; } diff --git a/tests/unit/Access/PermissionLimitsTest.php b/tests/unit/Access/PermissionLimitsTest.php new file mode 100644 index 000000000..58595111a --- /dev/null +++ b/tests/unit/Access/PermissionLimitsTest.php @@ -0,0 +1,78 @@ +<?php +/* + * Copyright (c) 2017 Hubzilla + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Zotlabs\Tests\Unit\Access; + +use phpmock\phpunit\PHPMock; +use Zotlabs\Tests\Unit\UnitTestCase; +use Zotlabs\Access\PermissionLimits; + +/** + * @brief Unit Test case for PermissionLimits class. + * + * @covers Zotlabs\Access\PermissionLimits + */ +class PermissionLimitsTest extends UnitTestCase { + + use PHPMock; + + /** + * @todo If we could replace static call to Permissions::Perms() in + * Std_Limits() we could better unit test this method, now we test the + * result of Permissions::Perms() mostly. + * + * @uses Zotlabs\Access\Permissions::Perms + * @uses ::call_hooks + */ + public function testStd_Limits() { + // There are 18 default perms + $permsCount = 18; + + // Create a stub for global function t() with expectation + $t = $this->getFunctionMock('Zotlabs\Access', 't'); + $t->expects($this->exactly($permsCount)); + + $stdlimits = PermissionLimits::Std_Limits(); + $this->assertCount($permsCount, $stdlimits, "There should be $permsCount permissions."); + + $this->assertEquals(PERMS_PUBLIC, $stdlimits['view_stream']); + $this->assertEquals(PERMS_SPECIFIC, $stdlimits['send_stream']); + $this->assertEquals(PERMS_PUBLIC, $stdlimits['view_profile']); + $this->assertEquals(PERMS_PUBLIC, $stdlimits['view_contacts']); + $this->assertEquals(PERMS_PUBLIC, $stdlimits['view_storage']); + $this->assertEquals(PERMS_SPECIFIC, $stdlimits['write_storage']); + $this->assertEquals(PERMS_PUBLIC, $stdlimits['view_pages']); + $this->assertEquals(PERMS_PUBLIC, $stdlimits['view_wiki']); + $this->assertEquals(PERMS_SPECIFIC, $stdlimits['write_pages']); + $this->assertEquals(PERMS_SPECIFIC, $stdlimits['write_wiki']); + $this->assertEquals(PERMS_SPECIFIC, $stdlimits['post_wall']); + $this->assertEquals(PERMS_PUBLIC, $stdlimits['post_comments']); + $this->assertEquals(PERMS_SPECIFIC, $stdlimits['post_mail']); + $this->assertEquals(PERMS_SPECIFIC, $stdlimits['post_like']); + $this->assertEquals(PERMS_SPECIFIC, $stdlimits['tag_deliver']); + $this->assertEquals(PERMS_SPECIFIC, $stdlimits['chat']); + $this->assertEquals(PERMS_SPECIFIC, $stdlimits['republish']); + $this->assertEquals(PERMS_SPECIFIC, $stdlimits['delegate']); + } + +}
\ No newline at end of file diff --git a/tests/unit/Access/PermissionRolesTest.php b/tests/unit/Access/PermissionRolesTest.php new file mode 100644 index 000000000..5e64e773a --- /dev/null +++ b/tests/unit/Access/PermissionRolesTest.php @@ -0,0 +1,100 @@ +<?php +/* + * Copyright (c) 2017 Hubzilla + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Zotlabs\Tests\Unit\Access; + +use Zotlabs\Tests\Unit\UnitTestCase; +use Zotlabs\Access\PermissionRoles; +use phpmock\phpunit\PHPMock; + +/** + * @brief Unit Test case for PermissionRoles class. + * + * @TODO Work around dependencies to static PermissionLimits methods. + * + * @covers Zotlabs\Access\PermissionRoles + */ +class PermissionRolesTest extends UnitTestCase { + + use PHPMock; + + public function testVersion() { + $expectedVersion = 2; + + $this->assertEquals($expectedVersion, PermissionRoles::version()); + + $pr = new PermissionRoles(); + $this->assertEquals($expectedVersion, $pr->version()); + } + + + public function testRoles() { + // Create a stub for global function t() with expectation + $t = $this->getFunctionMock('Zotlabs\Access', 't'); + $t->expects($this->atLeastOnce())->willReturnCallback( + function ($string) { + return $string; + } + ); + + $roles = PermissionRoles::roles(); + $r = new PermissionRoles(); + $this->assertEquals($roles, $r->roles()); + + $socialNetworking = [ + 'social' => 'Social - Mostly Public', + 'social_restricted' => 'Social - Restricted', + 'social_private' => 'Social - Private' + ]; + + $this->assertArraySubset(['Social Networking' => $socialNetworking], $roles); + $this->assertEquals($socialNetworking, $roles['Social Networking']); + + $this->assertCount(5, $roles, 'There should be 5 permission groups.'); + + $this->assertCount(1, $roles['Other'], "In the 'Other' group should be just one permission role"); + } + + + /** + * @uses ::call_hooks + * @uses Zotlabs\Access\PermissionLimits::Std_Limits + * @uses Zotlabs\Access\Permissions::Perms + */ + public function testRole_perms() { + // Create a stub for global function t() + $t = $this->getFunctionMock('Zotlabs\Access', 't'); + $t = $this->getFunctionMock('Zotlabs\Access', 'get_config'); + + $rp_social = PermissionRoles::role_perms('social'); + $this->assertEquals('social', $rp_social['role']); + + + $rp_custom = PermissionRoles::role_perms('custom'); + $this->assertEquals(['role' => 'custom'], $rp_custom); + + $rp_nonexistent = PermissionRoles::role_perms('nonexistent'); + $this->assertEquals(['role' => 'nonexistent'], $rp_nonexistent); + } + +}
\ No newline at end of file diff --git a/tests/unit/Access/PermissionsTest.php b/tests/unit/Access/PermissionsTest.php index 73d0e7827..40724fff8 100644 --- a/tests/unit/Access/PermissionsTest.php +++ b/tests/unit/Access/PermissionsTest.php @@ -23,6 +23,7 @@ namespace Zotlabs\Tests\Unit\Access; +use phpmock\phpunit\PHPMock; use Zotlabs\Tests\Unit\UnitTestCase; use Zotlabs\Access\Permissions; @@ -33,54 +34,228 @@ use Zotlabs\Access\Permissions; */ class PermissionsTest extends UnitTestCase { + use PHPMock; + + public function testVersion() { + $expectedVersion = 2; + + // static call + $this->assertEquals($expectedVersion, Permissions::version()); + + // instance call + $p = new Permissions(); + $this->assertEquals($expectedVersion, $p->version()); + } + + /** + * @coversNothing + */ + public function testVersionEqualsPermissionRoles() { + $p = new Permissions(); + $pr = new \Zotlabs\Access\PermissionRoles(); + $this->assertEquals($p->version(), $pr->version()); + } + + /** + * @uses ::call_hooks + */ + public function testPerms() { + // There are 18 default perms + $permsCount = 18; + + // Create a stub for global function t() with expectation + $t = $this->getFunctionMock('Zotlabs\Access', 't'); + $t->expects($this->exactly(2*$permsCount))->willReturnCallback( + function ($string) { + return $string; + } + ); + + // static method Perms() + $perms = Permissions::Perms(); + + $p = new Permissions(); + $this->assertEquals($perms, $p->Perms()); + + $this->assertEquals($permsCount, count($perms), "There should be $permsCount permissions."); + + $this->assertEquals('Can view my channel stream and posts', $perms['view_stream']); + + // non existent perm should not be set + $this->assertFalse(isset($perms['invalid_perm'])); + } + + /** + * filter parmeter is only used in hook \b permissions_list. So the result + * in this test should be the same as if there was no filter parameter. + * + * @todo Stub call_hooks() function and also test filter + * + * @uses ::call_hooks + */ + public function testPermsFilter() { + // There are 18 default perms + $permsCount = 18; + + // Create a stub for global function t() with expectation + $t = $this->getFunctionMock('Zotlabs\Access', 't'); + $t->expects($this->exactly(2*$permsCount))->willReturnCallback( + function ($string) { + return $string; + } + ); + + $perms = Permissions::Perms('view_'); + $this->assertEquals($permsCount, count($perms)); + + $this->assertEquals('Can view my channel stream and posts', $perms['view_stream']); + + $perms = Permissions::Perms('invalid_perm'); + $this->assertEquals($permsCount, count($perms)); + } + /** + * Better should mock Permissions::Perms, but not possible with static methods. + * + * @uses ::call_hooks + * * @dataProvider FilledPermsProvider + * + * @param array $permarr An indexed permissions array to pass + * @param array $expected The expected result perms array */ public function testFilledPerms($permarr, $expected) { - $this->markTestIncomplete( - 'Need to mock static function Permissions::Perms() ...' - ); - //$this->assertEquals($expected, Permissions::FilledPerms($permarr)); - -/* $perms = $this->getMockBuilder(Permissions::class) - ->setMethods(['Perms']) - ->getMock(); - $perms->expects($this->once()) - ->method('Perms'); - // still calls the static self::Perms() - $perms->FilledPerms($permarr); -*/ + // Create a stub for global function t() + $t = $this->getFunctionMock('Zotlabs\Access', 't'); + + $this->assertEquals($expected, Permissions::FilledPerms($permarr)); } + /** + * @return array An associative array with test values for FilledPerms() + * * \e array Indexed array which is passed as parameter to FilledPerms() + * * \e array Expected associative result array with filled perms + */ public function FilledPermsProvider() { return [ - 'empty' => [ + 'Empty param array' => [ [], - ['perm1' => 0, 'perm2' => 0] + [ + 'view_stream' => 0, + 'send_stream' => 0, + 'view_profile' => 0, + 'view_contacts' => 0, + 'view_storage' => 0, + 'write_storage' => 0, + 'view_pages' => 0, + 'view_wiki' => 0, + 'write_pages' => 0, + 'write_wiki' => 0, + 'post_wall' => 0, + 'post_comments' => 0, + 'post_mail' => 0, + 'post_like' => 0, + 'tag_deliver' => 0, + 'chat' => 0, + 'republish' => 0, + 'delegate' => 0 + ] ], - 'valid' => [ - [['perm1' => 1]], - ['perm1' => 1, 'perm2' => 0] + 'provide view_stream and view_pages as param' => [ + ['view_stream', 'view_pages'], + [ + 'view_stream' => 1, + 'send_stream' => 0, + 'view_profile' => 0, + 'view_contacts' => 0, + 'view_storage' => 0, + 'write_storage' => 0, + 'view_pages' => 1, + 'view_wiki' => 0, + 'write_pages' => 0, + 'write_wiki' => 0, + 'post_wall' => 0, + 'post_comments' => 0, + 'post_mail' => 0, + 'post_like' => 0, + 'tag_deliver' => 0, + 'chat' => 0, + 'republish' => 0, + 'delegate' => 0 + ] + ], + 'provide an unknown param' => [ + ['view_stream', 'unknown_perm'], + [ + 'view_stream' => 1, + 'send_stream' => 0, + 'view_profile' => 0, + 'view_contacts' => 0, + 'view_storage' => 0, + 'write_storage' => 0, + 'view_pages' => 0, + 'view_wiki' => 0, + 'write_pages' => 0, + 'write_wiki' => 0, + 'post_wall' => 0, + 'post_comments' => 0, + 'post_mail' => 0, + 'post_like' => 0, + 'tag_deliver' => 0, + 'chat' => 0, + 'republish' => 0, + 'delegate' => 0 + ] ] ]; } -/* public function testFilledPermsNull() { - // need to mock global function btlogger(); - Permissions::FilledPerms(null); + /** + * @uses ::call_hooks + */ + public function testFilledPermsNull() { + // Create a stub for global function t() with expectation + $t = $this->getFunctionMock('Zotlabs\Access', 't'); + $t->expects($this->atLeastOnce()); + // Create a stub for global function bt() with expectations + $bt = $this->getFunctionMock('Zotlabs\Access', 'btlogger'); + $bt->expects($this->once())->with($this->equalTo('FilledPerms: null')); + + $result = [ + 'view_stream' => 0, + 'send_stream' => 0, + 'view_profile' => 0, + 'view_contacts' => 0, + 'view_storage' => 0, + 'write_storage' => 0, + 'view_pages' => 0, + 'view_wiki' => 0, + 'write_pages' => 0, + 'write_wiki' => 0, + 'post_wall' => 0, + 'post_comments' => 0, + 'post_mail' => 0, + 'post_like' => 0, + 'tag_deliver' => 0, + 'chat' => 0, + 'republish' => 0, + 'delegate' => 0 + ]; + + $this->assertEquals($result, Permissions::FilledPerms(null)); } -*/ + /** * @dataProvider OPermsProvider * - * @param array $permarr - * @param array $expected + * @param array $permarr The params to pass to the OPerms method + * @param array $expected The expected result */ public function testOPerms($permarr, $expected) { $this->assertEquals($expected, Permissions::OPerms($permarr)); } /** - * @return Associative array with test values for OPerms() - * * \e array Array to test - * * \e array Expect array + * @return array An associative array with test values for OPerms() + * * \e array Array with perms to test + * * \e array Expected result array */ public function OPermsProvider() { return [ @@ -99,22 +274,21 @@ class PermissionsTest extends UnitTestCase { ]; } - /** * @dataProvider permsCompareProvider * - * @param array $p1 - * @param array $p2 - * @param boolean $expectedresult + * @param array $p1 The first permission + * @param array $p2 The second permission + * @param boolean $expectedresult The expected result of the tested method */ public function testPermsCompare($p1, $p2, $expectedresult) { $this->assertEquals($expectedresult, Permissions::PermsCompare($p1, $p2)); } /** - * @return Associative array with test values for PermsCompare() - * * \e array 1st array - * * \e array 2nd array - * * \e boolean expected result for the test + * @return array An associative array with test values for PermsCompare() + * * \e array 1st array with perms + * * \e array 2nd array with perms + * * \e boolean expected result for the perms comparison */ public function permsCompareProvider() { return [ diff --git a/view/theme/redbasic/js/redbasic.js b/view/theme/redbasic/js/redbasic.js index 3fee0f282..ed9ef02aa 100644 --- a/view/theme/redbasic/js/redbasic.js +++ b/view/theme/redbasic/js/redbasic.js @@ -2,7 +2,6 @@ * redbasic theme specific JavaScript */ -var notifications_parent; $(document).ready(function() { // CSS3 calc() fallback (for unsupported browsers) @@ -84,19 +83,6 @@ $(document).ready(function() { } }); - notifications_parent = $('#notifications_wrapper')[0].parentElement.id; - $('#notifications-btn').click(function() { - if($('#notifications_wrapper').hasClass('fs')) - $('#notifications_wrapper').prependTo('#' + notifications_parent); - else - $('#notifications_wrapper').prependTo('section'); - - $('#notifications_wrapper').toggleClass('fs'); - if($('#navbar-collapse-2').hasClass('show')){ - $('#navbar-collapse-2').removeClass('show'); - } - }); - $("input[data-role=cat-tagsinput]").tagsinput({ tagClass: 'badge badge-pill badge-warning text-dark' }); diff --git a/view/tpl/notifications_widget.tpl b/view/tpl/notifications_widget.tpl index 0ece84891..29892ba79 100644 --- a/view/tpl/notifications_widget.tpl +++ b/view/tpl/notifications_widget.tpl @@ -28,6 +28,22 @@ {{if $module == 'display'}} <script> + var notifications_parent; + $(document).ready(function() { + notifications_parent = $('#notifications_wrapper')[0].parentElement.id; + $('#notifications-btn').click(function() { + if($('#notifications_wrapper').hasClass('fs')) + $('#notifications_wrapper').prependTo('#' + notifications_parent); + else + $('#notifications_wrapper').prependTo('section'); + + $('#notifications_wrapper').toggleClass('fs'); + if($('#navbar-collapse-2').hasClass('show')){ + $('#navbar-collapse-2').removeClass('show'); + } + }); + }); + $(document).on('click', '.notification', function(e) { var b64mid = $(this).data('b64mid'); var path = $(this)[0].pathname.substr(1,7); diff --git a/view/tpl/photo_view.tpl b/view/tpl/photo_view.tpl index 2431329ec..105cf0ac8 100755 --- a/view/tpl/photo_view.tpl +++ b/view/tpl/photo_view.tpl @@ -92,7 +92,7 @@ <i id="jot-perms-icon" class="fa fa-{{$edit.lockstate}}"></i> </button> {{/if}} - <button id="dbtn-submit" class="btn btn-primary btn-sm" type="submit" name="submit" >{{$edit.submit}}</button> + <button id="tool-submit" class="btn btn-primary btn-sm" type="submit" name="submit" >{{$edit.submit}}</button> </div> </form> {{$edit.aclselect}} diff --git a/view/tpl/settings.tpl b/view/tpl/settings.tpl index 704d89bdd..33e0aa925 100755 --- a/view/tpl/settings.tpl +++ b/view/tpl/settings.tpl @@ -104,6 +104,9 @@ <div id="notification-settings-collapse" class="collapse" role="tabpanel" aria-labelledby="notification-settings"> <div class="section-content-tools-wrapper"> <div id="settings-notifications"> + + {{include file="field_input.tpl" field=$mailhost}} + <h3>{{$activity_options}}</h3> <div class="group"> {{*not yet implemented *}} |