Merge branch 'dev'

This commit is contained in:
zotlabs 2019-11-04 19:34:52 -08:00
commit 590d93d39f
38 changed files with 345 additions and 277 deletions

View file

@ -49,41 +49,41 @@ class AccessList {
static function remove($uid,$name) {
$ret = false;
if(x($uid) && x($name)) {
if ($uid && $name) {
$r = q("SELECT id, hash FROM pgrp WHERE uid = %d AND gname = '%s' LIMIT 1",
intval($uid),
dbesc($name)
);
if($r) {
if ($r) {
$group_id = $r[0]['id'];
$group_hash = $r[0]['hash'];
}
if(! $group_id)
else {
return false;
}
// remove group from default posting lists
$r = q("SELECT channel_default_group, channel_allow_gid, channel_deny_gid FROM channel WHERE channel_id = %d LIMIT 1",
intval($uid)
);
if($r) {
$user_info = $r[0];
if ($r) {
$user_info = array_shift($r);
$change = false;
if($user_info['channel_default_group'] == $group_hash) {
if ($user_info['channel_default_group'] == $group_hash) {
$user_info['channel_default_group'] = '';
$change = true;
}
if(strpos($user_info['channel_allow_gid'], '<' . $group_hash . '>') !== false) {
if (strpos($user_info['channel_allow_gid'], '<' . $group_hash . '>') !== false) {
$user_info['channel_allow_gid'] = str_replace('<' . $group_hash . '>', '', $user_info['channel_allow_gid']);
$change = true;
}
if(strpos($user_info['channel_deny_gid'], '<' . $group_hash . '>') !== false) {
if (strpos($user_info['channel_deny_gid'], '<' . $group_hash . '>') !== false) {
$user_info['channel_deny_gid'] = str_replace('<' . $group_hash . '>', '', $user_info['channel_deny_gid']);
$change = true;
}
if($change) {
if ($change) {
q("UPDATE channel SET channel_default_group = '%s', channel_allow_gid = '%s', channel_deny_gid = '%s'
WHERE channel_id = %d",
intval($user_info['channel_default_group']),
@ -119,19 +119,21 @@ class AccessList {
// or false.
static function byname($uid,$name) {
if((! $uid) || (! strlen($name)))
if (! ($uid && $name)) {
return false;
$r = q("SELECT * FROM pgrp WHERE uid = %d AND gname = '%s' LIMIT 1",
}
$r = q("SELECT id FROM pgrp WHERE uid = %d AND gname = '%s' LIMIT 1",
intval($uid),
dbesc($name)
);
if($r)
if ($r) {
return $r[0]['id'];
}
return false;
}
static function by_id($uid,$id) {
if((! $uid) || (! $id)) {
if (! ($uid && $id)) {
return false;
}
@ -139,34 +141,37 @@ class AccessList {
intval($uid),
intval($id)
);
if($r) {
if ($r) {
return array_shift($r);
}
return false;
}
static function rec_byhash($uid,$hash) {
if((! $uid) || (! strlen($hash)))
if (! ( $uid && $hash)) {
return false;
}
$r = q("SELECT * FROM pgrp WHERE uid = %d AND hash = '%s' LIMIT 1",
intval($uid),
dbesc($hash)
);
if($r)
return $r[0];
if ($r) {
return array_shift($r);
}
return false;
}
static function member_remove($uid,$name,$member) {
$gid = self::byname($uid,$name);
if(! $gid)
if (! $gid) {
return false;
if(! ( $uid && $gid && $member))
}
if (! ($uid && $gid && $member)) {
return false;
}
$r = q("DELETE FROM pgrp_member WHERE uid = %d AND gid = %d AND xchan = '%s' ",
intval($uid),
intval($gid),
@ -180,37 +185,39 @@ class AccessList {
static function member_add($uid,$name,$member,$gid = 0) {
if(! $gid)
if (! $gid) {
$gid = self::byname($uid,$name);
if((! $gid) || (! $uid) || (! $member))
}
if (! ($gid && $uid && $member)) {
return false;
}
$r = q("SELECT * FROM pgrp_member WHERE uid = %d AND gid = %d AND xchan = '%s' LIMIT 1",
intval($uid),
intval($gid),
dbesc($member)
);
if($r)
if ($r) {
return true; // You might question this, but
// we indicate success because the group member was in fact created
// -- It was just created at another time
if(! $r)
}
else {
$r = q("INSERT INTO pgrp_member (uid, gid, xchan)
VALUES( %d, %d, '%s' ) ",
intval($uid),
intval($gid),
dbesc($member)
);
);
}
Libsync::build_sync_packet($uid,null,true);
return $r;
}
static function members($uid, $gid) {
$ret = array();
if(intval($gid)) {
$ret = [];
if (intval($gid)) {
$r = q("SELECT * FROM pgrp_member
LEFT JOIN abook ON abook_xchan = pgrp_member.xchan left join xchan on xchan_hash = abook_xchan
WHERE gid = %d AND abook_channel = %d and pgrp_member.uid = %d and xchan_deleted = 0 and abook_self = 0 and abook_blocked = 0 and abook_pending = 0 ORDER BY xchan_name ASC ",
@ -218,22 +225,23 @@ class AccessList {
intval($uid),
intval($uid)
);
if($r)
if ($r) {
$ret = $r;
}
}
return $ret;
}
static function members_xchan($uid,$gid) {
$ret = [];
if(intval($gid)) {
if (intval($gid)) {
$r = q("SELECT xchan FROM pgrp_member WHERE gid = %d AND uid = %d",
intval($gid),
intval($uid)
);
if($r) {
foreach($r as $rr) {
$ret[] = $rr['xchan'];
if ($r) {
foreach ($r as $rv) {
$ret[] = $rv['xchan'];
}
}
}
@ -242,15 +250,14 @@ class AccessList {
static function members_profile_xchan($uid,$gid) {
$ret = [];
if(intval($gid)) {
if (intval($gid)) {
$r = q("SELECT abook_xchan as xchan from abook left join profile on abook_profile = profile_guid where profile.id = %d and profile.uid = %d",
intval($gid),
intval($uid)
);
if($r) {
foreach($r as $rr) {
$ret[] = $rr['xchan'];
if ($r) {
foreach($r as $rv) {
$ret[] = $rv['xchan'];
}
}
}
@ -263,30 +270,25 @@ class AccessList {
static function select($uid,$group = '') {
$grps = [];
$o = '';
$r = q("SELECT * FROM pgrp WHERE deleted = 0 AND uid = %d ORDER BY gname ASC",
intval($uid)
);
$grps[] = array('name' => '', 'hash' => '0', 'selected' => '');
if($r) {
foreach($r as $rr) {
$grps[] = array('name' => $rr['gname'], 'id' => $rr['hash'], 'selected' => (($group == $rr['hash']) ? 'true' : ''));
$grps[] = [ 'name' => '', 'hash' => '0', 'selected' => '' ];
if ($r) {
foreach ($r as $rr) {
$grps[] = [ 'name' => $rr['gname'], 'id' => $rr['hash'], 'selected' => (($group == $rr['hash']) ? 'true' : '') ];
}
}
logger('select: ' . print_r($grps,true), LOGGER_DATA);
$o = replace_macros(get_markup_template('group_selection.tpl'), array(
return replace_macros(get_markup_template('group_selection.tpl'), [
'$label' => t('Add new connections to this access list'),
'$groups' => $grps
));
return $o;
]);
}
static function widget($every="connections",$each="lists",$edit = false, $group_id = 0, $cid = '',$mode = 1) {
$o = '';
@ -297,12 +299,12 @@ class AccessList {
intval($_SESSION['uid'])
);
$member_of = [];
if($cid) {
if ($cid) {
$member_of = self::containing(local_channel(),$cid);
}
if($r) {
foreach($r as $rr) {
if ($r) {
foreach ($r as $rr) {
$selected = (($group_id == $rr['id']) ? ' group-selected' : '');
if ($edit) {
@ -324,41 +326,38 @@ class AccessList {
];
}
}
$tpl = get_markup_template("group_side.tpl");
$o = replace_macros($tpl, array(
return replace_macros(get_markup_template('group_side.tpl'), [
'$title' => t('Lists'),
'$edittext' => t('Edit list'),
'$createtext' => t('Create new list'),
'$ungrouped' => (($every === 'contacts') ? t('Channels not in any access list') : ''),
'$groups' => $groups,
'$add' => t('add'),
));
return $o;
]);
}
static function expand($g) {
if(! (is_array($g) && count($g)))
return array();
if (! (is_array($g) && count($g))) {
return [];
}
$ret = [];
$x = [];
// private profile linked virtual groups
foreach($g as $gv) {
if(substr($gv,0,3) === 'vp.') {
foreach ($g as $gv) {
if (substr($gv,0,3) === 'vp.') {
$profile_hash = substr($gv,3);
if($profile_hash) {
if ($profile_hash) {
$r = q("select abook_xchan from abook where abook_profile = '%s'",
dbesc($profile_hash)
);
if($r) {
foreach($r as $rv) {
if ($r) {
foreach ($r as $rv) {
$ret[] = $rv['abook_xchan'];
}
}
@ -369,14 +368,14 @@ class AccessList {
}
}
if($x) {
if ($x) {
stringify_array_elms($x,true);
$groups = implode(',', $x);
if($groups) {
if ($groups) {
$r = q("SELECT xchan FROM pgrp_member WHERE gid IN ( select id from pgrp where hash in ( $groups ))");
if($r) {
foreach($r as $rr) {
$ret[] = $rr['xchan'];
if ($r) {
foreach ($r as $rv) {
$ret[] = $rv['xchan'];
}
}
}
@ -386,12 +385,12 @@ class AccessList {
static function member_of($c) {
$r = q("SELECT pgrp.gname, pgrp.id FROM pgrp LEFT JOIN pgrp_member ON pgrp_member.gid = pgrp.id WHERE pgrp_member.xchan = '%s' AND pgrp.deleted = 0 ORDER BY pgrp.gname ASC ",
$r = q("SELECT pgrp.gname, pgrp.id FROM pgrp LEFT JOIN pgrp_member ON pgrp_member.gid = pgrp.id
WHERE pgrp_member.xchan = '%s' AND pgrp.deleted = 0 ORDER BY pgrp.gname ASC ",
dbesc($c)
);
return $r;
}
static function containing($uid,$c) {
@ -401,12 +400,12 @@ class AccessList {
dbesc($c)
);
$ret = array();
if($r) {
foreach($r as $rr)
$ret[] = $rr['gid'];
$ret = [];
if ($r) {
foreach ($r as $rv)
$ret[] = $rv['gid'];
}
return $ret;
}
}

View file

@ -34,7 +34,7 @@ class Activity {
// Eventually this needs to be passed in much further up the stack
// and base the decision on whether or not we are encoding for ActivityPub or Zot6
return self::fetch_item($x,((get_config('system','activitypub')) ? true : false));
return self::fetch_item($x,((get_config('system','activitypub',true)) ? true : false));
}
if ($x['type'] === ACTIVITY_OBJ_THING) {
return self::fetch_thing($x);
@ -87,8 +87,8 @@ class Activity {
$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'
'Date' => datetime_convert('UTC','UTC', 'now', 'D, d M Y h:i:s \\G\\M\\T'),
'(request-target)' => 'get ' . get_request_string($url)
];
if (isset($token)) {
$headers['Authorization'] = 'Bearer ' . $token;
@ -244,7 +244,11 @@ class Activity {
$ret = [];
if ($item['tag'] && is_array($item['tag'])) {
foreach ($item['tag'] as $t) {
$ptr = $item['tag'];
if (! array_key_exists(0,$ptr)) {
$ptr = [ $ptr ];
}
foreach ($ptr as $t) {
if (! array_key_exists('type',$t))
$t['type'] = 'Hashtag';
@ -399,8 +403,12 @@ class Activity {
$ret = [];
if ($item['attachment']) {
foreach ($item['attachment'] as $att) {
if (is_array($item['attachment']) && $item['attachment']) {
$ptr = $item['attachment'];
if (! array_key_exists(0,$ptr)) {
$ptr = [ $ptr ];
}
foreach ($ptr as $att) {
$entry = [];
if ($att['href'])
$entry['href'] = $att['href'];
@ -414,6 +422,9 @@ class Activity {
$ret[] = $entry;
}
}
else {
btlogger('not an array: ' . $item['attachment']);
}
return $ret;
}
@ -736,7 +747,7 @@ class Activity {
if ($d) {
$recips = get_iconfig($i['parent'], 'activitypub', 'recips');
if (in_array($i['author']['xchan_url'], $recips['to'])) {
if (is_array($recips) && in_array($i['author']['xchan_url'], $recips['to'])) {
$reply_url = $d[0]['xchan_url'];
$is_directmessage = true;
}
@ -972,7 +983,7 @@ class Activity {
];
$ret['url'] = $p['xchan_url'];
if ($activitypub && get_config('system','activitypub')) {
if ($activitypub && get_config('system','activitypub',true)) {
if ($c) {
if (get_pconfig($c['channel_id'],'system','activitypub',true)) {
@ -2079,7 +2090,6 @@ class Activity {
}
$allowed = false;
$moderated = false;
if ($is_child_node) {
$p = q("select id from item where mid = '%s' and uid = %d and item_wall = 1",
@ -2096,6 +2106,10 @@ class Activity {
self::send_rejection_activity($channel,$item['author_xchan'],$item);
return;
}
if (perm_is_allowed($channel['channel_id'],$item['author_xchan'],'moderated')) {
$item['item_blocked'] = ITEM_MODERATED;
}
}
else {
$allowed = true;
@ -2140,14 +2154,6 @@ class Activity {
return;
}
if (is_array($act->obj)) {
$content = self::get_content($act->obj);
}
if (! $content) {
logger('no content');
return;
}
$item['aid'] = $channel['channel_account_id'];
$item['uid'] = $channel['channel_id'];
@ -2216,7 +2222,7 @@ class Activity {
intval($item['uid'])
);
if (! $p) {
if (! get_config('system','activitypub')) {
if (! get_config('system','activitypub',true)) {
return;
}
else {

View file

@ -69,7 +69,7 @@ class Connect {
$xchan_hash = '';
$sql_options = (($protocol) ? " and xchan_network = '" . dbesc($protocol) . "' " : '');
$r = q("select * from xchan where xchan_hash = '%s' or xchan_url = '%s' or xchan_addr = '%s' $sql_options ",
$r = q("select * from xchan where ( xchan_hash = '%s' or xchan_url = '%s' or xchan_addr = '%s') $sql_options ",
dbesc($url),
dbesc($url),
dbesc($url)
@ -85,7 +85,7 @@ class Connect {
// Some Hubzilla records were originally stored as activitypub. If we find one, force rediscovery
// since Zap cannot connect with them.
if ($r['xchan_network'] === 'activitypub' && ! get_config('system','activitypub')) {
if ($r['xchan_network'] === 'activitypub' && ! get_config('system','activitypub',true)) {
$r = null;
}
}
@ -117,7 +117,7 @@ class Connect {
// something was discovered - find the record which was just created.
$r = q("select * from xchan where xchan_hash = '%s' or xchan_url = '%s' or xchan_addr = '%s' $sql_options",
$r = q("select * from xchan where ( xchan_hash = '%s' or xchan_url = '%s' or xchan_addr = '%s' ) $sql_options",
dbesc(($wf) ? $wf : $url),
dbesc($url),
dbesc($url)
@ -153,7 +153,7 @@ class Connect {
}
$ap_allowed = get_config('system','activitypub',false) && get_pconfig($uid,'system','activitypub',true);
$ap_allowed = get_config('system','activitypub',true) && get_pconfig($uid,'system','activitypub',true);
if ($r['xchan_network'] === 'activitypub') {
if (! $ap_allowed) {
@ -297,7 +297,7 @@ class Connect {
);
if ($r) {
$result['abook'] = $r[0];
$result['abook'] = array_shift($r);
Master::Summon([ 'Notifier', 'permissions_create', $result['abook']['abook_id'] ]);
}

View file

@ -237,6 +237,7 @@ class Queue {
$headers['Content-Type'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ;
$ret = $outq['outq_msg'];
logger('ActivityPub send: ' . jindent($ret), LOGGER_DATA);
$headers['Date'] = datetime_convert('UTC','UTC', 'now', 'D, d M Y h:i:s \\G\\M\\T');
$headers['Digest'] = HTTPSig::generate_digest_header($ret);
$headers['(request-target)'] = 'post ' . get_request_string($outq['outq_posturl']);

View file

@ -507,7 +507,7 @@ class ThreadItem {
$result['children'] = [];
if (get_config('system','activitypub') && local_channel() && get_pconfig(local_channel(),'system','activitypub',true)) {
if (get_config('system','activitypub',true) && local_channel() && get_pconfig(local_channel(),'system','activitypub',true)) {
// place to store all the author addresses (links if not available) in the thread so we can auto-mention them in JS.
$result['authors'] = [];
// fix to add in sub-replies if replying to a comment on your own post from the top level.

View file

@ -87,13 +87,14 @@ class Activity extends Controller {
ACTIVITYSTREAMS_JSONLD_REV,
'https://w3id.org/security/v1',
z_root() . ZOT_APSCHEMA_REV
]], ZlibActivity::encode_activity($items[0]));
]], ZlibActivity::encode_activity($items[0],get_config('system','activitypub',true)));
$headers = [];
$headers['Content-Type'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ;
$x['signature'] = LDSignatures::sign($x,$channel);
$ret = json_encode($x, JSON_UNESCAPED_SLASHES);
$headers['Date'] = datetime_convert('UTC','UTC', 'now', 'D, d M Y h:i:s \\G\\M\\T');
$headers['Digest'] = HTTPSig::generate_digest_header($ret);
$headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI'];

View file

@ -309,7 +309,7 @@ class Site {
// '$theme_mobile' => [ 'theme_mobile', t("Mobile system theme"), get_config('system','mobile_theme'), t("Theme for mobile devices"), $theme_choices_mobile ],
// '$site_channel' => [ 'site_channel', t("Channel to use for this website's static pages"), get_config('system','site_channel'), t("Site Channel") ],
'$feed_contacts' => [ 'feed_contacts', t('Allow Feeds as Connections'),get_config('system','feed_contacts'),t('(Heavy system resource usage)') ],
'$ap_contacts' => [ 'ap_contacts', t('Allow ActivityPub Connections'),get_config('system','activitypub'),t('Provides basic conversational access to software supporting the ActivityPub protocol.') ],
'$ap_contacts' => [ 'ap_contacts', t('Allow ActivityPub Connections'),get_config('system','activitypub',true),t('Provides basic conversational access to software supporting the ActivityPub protocol.') ],
'$maximagesize' => [ 'maximagesize', t("Maximum image size"), intval(get_config('system','maximagesize')), t("Maximum size in bytes of uploaded images. Default is 0, which means no limits.") ],
'$register_policy' => [ 'register_policy', t("Does this site allow new member registration?"), get_config('system','register_policy'), "", $register_choices ],
'$invite_only' => [ 'invite_only', t("Invitation only"), get_config('system','invitation_only'), t("Only allow new member registrations with an invitation code. New member registration must be allowed for this to work.") ],

View file

@ -121,6 +121,7 @@ class Channel extends Controller {
$x['signature'] = LDSignatures::sign($x,$channel);
$ret = json_encode($x, JSON_UNESCAPED_SLASHES);
$headers['Date'] = datetime_convert('UTC','UTC', 'now', 'D, d M Y h:i:s \\G\\M\\T');
$headers['Digest'] = HTTPSig::generate_digest_header($ret);
$headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI'];
$h = HTTPSig::create_sig($headers,$channel['channel_prvkey'],channel_url($channel));

View file

@ -59,6 +59,7 @@ class Event extends Controller {
$headers['Content-Type'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ;
$x['signature'] = LDSignatures::sign($x,$channel);
$ret = json_encode($x, JSON_UNESCAPED_SLASHES);
$headers['Date'] = datetime_convert('UTC','UTC', 'now', 'D, d M Y h:i:s \\G\\M\\T');
$headers['Digest'] = HTTPSig::generate_digest_header($ret);
$headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI'];

View file

@ -55,6 +55,7 @@ class Follow extends Controller {
$headers['Content-Type'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ;
$x['signature'] = LDSignatures::sign($x,$chan);
$ret = json_encode($x, JSON_UNESCAPED_SLASHES);
$headers['Date'] = datetime_convert('UTC','UTC', 'now', 'D, d M Y h:i:s \\G\\M\\T');
$headers['Digest'] = HTTPSig::generate_digest_header($ret);
$headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI'];
$h = HTTPSig::create_sig($headers,$chan['channel_prvkey'],channel_url($chan));

View file

@ -59,6 +59,7 @@ class Followers extends Controller {
$headers['Content-Type'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ;
$x['signature'] = LDSignatures::sign($x,$channel);
$ret = json_encode($x, JSON_UNESCAPED_SLASHES);
$headers['Date'] = datetime_convert('UTC','UTC', 'now', 'D, d M Y h:i:s \\G\\M\\T');
$headers['Digest'] = HTTPSig::generate_digest_header($ret);
$headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI'];
$h = HTTPSig::create_sig($headers,$channel['channel_prvkey'],channel_url($channel));

View file

@ -56,6 +56,7 @@ class Following extends Controller {
$headers['Content-Type'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ;
$x['signature'] = LDSignatures::sign($x,$channel);
$ret = json_encode($x, JSON_UNESCAPED_SLASHES);
$headers['Date'] = datetime_convert('UTC','UTC', 'now', 'D, d M Y h:i:s \\G\\M\\T');
$headers['Digest'] = HTTPSig::generate_digest_header($ret);
$headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI'];
$h = HTTPSig::create_sig($headers,$channel['channel_prvkey'],channel_url($channel));

View file

@ -79,20 +79,8 @@ class Help extends Controller {
killme();
}
$headings = [
'about' => t('About'),
'member' => t('Members'),
'admin' => t('Administrators'),
'developer' => t('Developers'),
'tutorials' => t('Tutorials')
];
if(array_key_exists(argv(1), $headings))
$heading = $headings[argv(1)];
$content = get_help_content();
$language = determine_help_language()['language'];
return replace_macros(get_markup_template('help.tpl'), array(
'$title' => t('$Projectname Documentation'),

View file

@ -90,7 +90,7 @@ class Id extends Controller {
xchan_query($r,true);
$items = fetch_post_tags($r,true);
$i = Activity::encode_item($items[0],( get_config('system','activitypub') ? true : false ));
$i = Activity::encode_item($items[0],( get_config('system','activitypub',true) ? true : false ));
if(! $i)
http_status_exit(404, 'Not found');

View file

@ -90,7 +90,7 @@ class Item extends Controller {
dbesc($portable_id)
);
}
elseif (! Config::get('system','require_authenticated_fetch',false)) {
elseif (Config::get('system','require_authenticated_fetch',false)) {
http_status_exit(403,'Permission denied');
}
@ -122,7 +122,7 @@ class Item extends Controller {
if(! perm_is_allowed($chan['channel_id'],get_observer_hash(),'view_stream'))
http_status_exit(403, 'Forbidden');
$i = Activity::encode_item($items[0],((get_config('system','activitypub')) ? true : false));
$i = Activity::encode_item($items[0],((get_config('system','activitypub',true)) ? true : false));
if(! $i)
http_status_exit(404, 'Not found');
@ -139,6 +139,7 @@ class Item extends Controller {
$headers['Content-Type'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ;
$x['signature'] = LDSignatures::sign($x,$chan);
$ret = json_encode($x, JSON_UNESCAPED_SLASHES);
$headers['Date'] = datetime_convert('UTC','UTC', 'now', 'D, d M Y h:i:s \\G\\M\\T');
$headers['Digest'] = HTTPSig::generate_digest_header($ret);
$headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI'];
$h = HTTPSig::create_sig($headers,$chan['channel_prvkey'],channel_url($chan));
@ -192,7 +193,7 @@ class Item extends Controller {
dbesc($portable_id)
);
}
elseif (! Config::get('system','require_authenticated_fetch',false)) {
elseif (Config::get('system','require_authenticated_fetch',false)) {
http_status_exit(403,'Permission denied');
}
@ -989,11 +990,6 @@ class Item extends Controller {
'revision' => $r['data']['revision']
);
}
$ext = substr($r['data']['filename'],strrpos($r['data']['filename'],'.'));
if(strpos($r['data']['filetype'],'audio/') !== false)
$attach_link = '[audio]' . z_root() . '/attach/' . $r['data']['hash'] . '/' . $r['data']['revision'] . (($ext) ? $ext : '') . '[/audio]';
elseif(strpos($r['data']['filetype'],'video/') !== false)
$attach_link = '[video]' . z_root() . '/attach/' . $r['data']['hash'] . '/' . $r['data']['revision'] . (($ext) ? $ext : '') . '[/video]';
$body = str_replace($match[1][$i],$attach_link,$body);
$i++;
}

View file

@ -41,7 +41,7 @@ class Lists extends Controller {
}
observer_auth($portable_id);
}
elseif (! Config::get('system','require_authenticated_fetch',false)) {
elseif (Config::get('system','require_authenticated_fetch',false)) {
http_status_exit(403,'Permission denied');
}
@ -74,6 +74,7 @@ class Lists extends Controller {
$headers['Content-Type'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ;
$x['signature'] = LDSignatures::sign($x,$channel);
$ret = json_encode($x, JSON_UNESCAPED_SLASHES);
$headers['Date'] = datetime_convert('UTC','UTC', 'now', 'D, d M Y h:i:s \\G\\M\\T');
$headers['Digest'] = HTTPSig::generate_digest_header($ret);
$headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI'];
$h = HTTPSig::create_sig($headers,$channel['channel_prvkey'],channel_url($channel));

View file

@ -76,6 +76,7 @@ class Outbox extends Controller {
$headers['Content-Type'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ;
$x['signature'] = LDSignatures::sign($x,$channel);
$ret = json_encode($x, JSON_UNESCAPED_SLASHES);
$headers['Date'] = datetime_convert('UTC','UTC', 'now', 'D, d M Y h:i:s \\G\\M\\T');
$headers['Digest'] = HTTPSig::generate_digest_header($ret);
$headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI'];
$h = HTTPSig::create_sig($headers,$channel['channel_prvkey'],channel_url($channel));

View file

@ -39,7 +39,7 @@ class Photo extends Controller {
$channel = channelx_by_n($r[0]['uid']);
$obj = json_decode($r[0]['obj'],true);
$obj['actor'] = Activity::encode_person($channel,true,((get_config('system','activitypub')) ? true : false));
$obj['actor'] = Activity::encode_person($channel,true,((get_config('system','activitypub',true)) ? true : false));
$x = array_merge(['@context' => [
ACTIVITYSTREAMS_JSONLD_REV,
@ -52,6 +52,7 @@ class Photo extends Controller {
$x['signature'] = LDSignatures::sign($x,$channel);
$ret = json_encode($x, JSON_UNESCAPED_SLASHES);
$headers['Date'] = datetime_convert('UTC','UTC', 'now', 'D, d M Y h:i:s \\G\\M\\T');
$headers['Digest'] = HTTPSig::generate_digest_header($ret);
$headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI'];
$h = HTTPSig::create_sig($headers,$channel['channel_prvkey'],channel_url($channel));

View file

@ -75,7 +75,7 @@ class Profile extends \Zotlabs\Web\Controller {
$chan = channelx_by_nick(argv(1));
if(! $chan)
http_status_exit(404, 'Not found');
$p = Activity::encode_person($chan,true,((get_config('system','activitypub')) ? true : false));
$p = Activity::encode_person($chan,true,((get_config('system','activitypub',true)) ? true : false));
if(! $p) {
http_status_exit(404, 'Not found');
}
@ -93,6 +93,7 @@ class Profile extends \Zotlabs\Web\Controller {
$headers['Content-Type'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ;
$x['signature'] = LDSignatures::sign($x,$chan);
$ret = json_encode($x, JSON_UNESCAPED_SLASHES);
$headers['Date'] = datetime_convert('UTC','UTC', 'now', 'D, d M Y h:i:s \\G\\M\\T');
$headers['Digest'] = HTTPSig::generate_digest_header($ret);
$headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI'];
$h = HTTPSig::create_sig($headers,$chan['channel_prvkey'],channel_url($chan));

View file

@ -516,7 +516,7 @@ class Channel {
$hyperdrive = [ 'hyperdrive', t('Friend-of-friend conversations'), ((get_pconfig(local_channel(),'system','hyperdrive',true)) ? 1 : 0), t('Import public third-party conversations in which your connections participate.'), $yes_no ];
if (get_config('system','activitypub')) {
if (get_config('system','activitypub',true)) {
$apconfig = true;
$activitypub = replace_macros(get_markup_template('field_checkbox.tpl'), [ '$field' => [ 'activitypub', t('Enable ActivityPub protocol'), ((get_pconfig(local_channel(),'system','activitypub',true)) ? 1 : 0), t(''), $yes_no ]]);
}

View file

@ -17,7 +17,7 @@ class Siteinfo extends Controller {
function get() {
$federated = 'Zot6';
if (Config::get('system','activitypub')) {
if (Config::get('system','activitypub',true)) {
$federated .= ', ActivityPub';
}
@ -27,6 +27,7 @@ class Siteinfo extends Controller {
$siteinfo = replace_macros(get_markup_template('siteinfo.tpl'),
[
'$title' => t('About this site'),
'$url' => z_root(),
'$sitenametxt' => t('Site Name'),
'$sitename' => System::get_site_name(),
'$headline' => t('Site Information'),

View file

@ -129,8 +129,9 @@ class Wall_attach extends Controller {
$s .= "\n\n" . '[attachment]' . $r['data']['hash'] . ',' . $r['data']['revision'] . '[/attachment]' . "\n";
}
if ($using_api)
if ($using_api) {
return $s;
}
$result['message'] = $s;
json_return_and_die($result);

View file

@ -111,7 +111,7 @@ class Webfinger extends Controller {
'http://webfinger.net/ns/name' => $channel_target['channel_name'],
'http://xmlns.com/foaf/0.1/name' => $channel_target['channel_name'],
'https://w3id.org/security/v1#publicKeyPem' => $channel_target['xchan_pubkey'],
'http://purl.org/zot/federation' => ((get_config('system','activitypub')) ? 'zot6,activitypub' : 'zot6')
'http://purl.org/zot/federation' => ((get_config('system','activitypub',true)) ? 'zot6,activitypub' : 'zot6')
];
foreach ($aliases as $alias) {

View file

@ -50,7 +50,7 @@ class Well_known extends Controller {
break;
case 'dnt-policy.txt':
echo file_get_contents('doc/dnt-policy.txt');
echo file_get_contents('doc/global/dnt-policy.txt');
killme();
default:

View file

@ -6,6 +6,7 @@ namespace Zotlabs\Photo;
* @brief GD photo driver.
*
*/
class PhotoGd extends PhotoDriver {
/**
@ -15,21 +16,27 @@ class PhotoGd extends PhotoDriver {
public function supportedTypes() {
$t = [];
$t['image/jpeg'] = 'jpg';
if(imagetypes() & IMG_PNG)
if (imagetypes() & IMG_PNG) {
$t['image/png'] = 'png';
if(imagetypes() & IMG_GIF)
}
if (imagetypes() & IMG_GIF) {
$t['image/gif'] = 'gif';
}
if (imagetypes() & IMG_WEBP) {
$t['image/webp'] = 'webp';
}
return $t;
}
protected function load($data, $type) {
$this->valid = false;
if(! $data)
if (! $data) {
return;
}
$this->image = @imagecreatefromstring($data);
if($this->image !== false) {
if ($this->image !== false) {
$this->valid = true;
$this->setDimensions();
imagealphablending($this->image, false);
@ -52,7 +59,7 @@ class PhotoGd extends PhotoDriver {
}
protected function destroy() {
if($this->is_valid()) {
if ($this->is_valid()) {
imagedestroy($this->image);
}
}
@ -65,8 +72,9 @@ class PhotoGd extends PhotoDriver {
* @return boolean|resource
*/
public function getImage() {
if(! $this->is_valid())
if (! $this->is_valid()) {
return false;
}
return $this->image;
}
@ -79,39 +87,42 @@ class PhotoGd extends PhotoDriver {
imagealphablending($dest, false);
imagesavealpha($dest, true);
if($this->type == 'image/png')
if ($this->type == 'image/png') {
imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
}
imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height);
if($this->image)
if ($this->image) {
imagedestroy($this->image);
}
$this->image = $dest;
$this->setDimensions();
}
public function rotate($degrees) {
if(! $this->is_valid())
if (! $this->is_valid()) {
return false;
}
$this->image = imagerotate($this->image, $degrees, 0);
$this->setDimensions();
}
public function flip($horiz = true, $vert = false) {
if(! $this->is_valid())
if (! $this->is_valid()) {
return false;
}
$w = imagesx($this->image);
$h = imagesy($this->image);
$flipped = imagecreate($w, $h);
if($horiz) {
for($x = 0; $x < $w; $x++) {
if ($horiz) {
for ($x = 0; $x < $w; $x++) {
imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h);
}
}
if($vert) {
for($y = 0; $y < $h; $y++) {
if ($vert) {
for ($y = 0; $y < $h; $y++) {
imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1);
}
}
@ -120,19 +131,20 @@ class PhotoGd extends PhotoDriver {
}
public function cropImageRect($maxx, $maxy, $x, $y, $w, $h) {
if(! $this->is_valid())
if (! $this->is_valid()) {
return false;
}
$dest = imagecreatetruecolor($maxx, $maxy);
imagealphablending($dest, false);
imagesavealpha($dest, true);
if($this->type == 'image/png')
if ($this->type == 'image/png') {
imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
}
imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $maxx, $maxy, $w, $h);
if($this->image)
if ($this->image) {
imagedestroy($this->image);
}
$this->image = $dest;
$this->setDimensions();
}
@ -142,18 +154,25 @@ class PhotoGd extends PhotoDriver {
* @see \Zotlabs\Photo\PhotoDriver::imageString()
*/
public function imageString() {
if(! $this->is_valid())
if (! $this->is_valid()) {
return false;
}
$quality = false;
ob_start();
switch($this->getType()){
switch ($this->getType()){
case 'image/webp':
\imagewebp($this->image);
break;
case 'image/png':
$quality = get_config('system', 'png_quality');
if((! $quality) || ($quality > 9))
if ((! $quality) || ($quality > 9)) {
$quality = PNG_QUALITY;
}
\imagepng($this->image, NULL, $quality);
break;
@ -161,8 +180,9 @@ class PhotoGd extends PhotoDriver {
// gd can lack imagejpeg(), but we verify during installation it is available
default:
$quality = get_config('system', 'jpeg_quality');
if((! $quality) || ($quality > 100))
if ((! $quality) || ($quality > 100)) {
$quality = JPEG_QUALITY;
}
\imagejpeg($this->image, NULL, $quality);
break;

View file

@ -2,6 +2,10 @@
namespace Zotlabs\Photo;
use Imagick;
use Exception;
/**
* @brief ImageMagick photo driver.
*/
@ -10,30 +14,33 @@ class PhotoImagick extends PhotoDriver {
public function supportedTypes() {
return [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
];
}
private function get_FormatsMap() {
return [
'image/jpeg' => 'JPG',
'image/png' => 'PNG',
'image/gif' => 'GIF',
'image/png' => 'PNG',
'image/gif' => 'GIF',
'image/webp' => 'WEBP'
];
}
protected function load($data, $type) {
$this->valid = false;
$this->image = new \Imagick();
$this->image = new Imagick();
if(! $data)
if (! $data) {
return;
}
try {
$this->image->readImageBlob($data);
} catch(\Exception $e) {
} catch(Exception $e) {
logger('Imagick readImageBlob() exception:' . print_r($e, true));
return;
}
@ -45,7 +52,7 @@ class PhotoImagick extends PhotoDriver {
$map = $this->get_FormatsMap();
$format = $map[$type];
if($this->image) {
if ($this->image) {
$this->image->setFormat($format);
// Always coalesce, if it is not a multi-frame image it won't hurt anyway
@ -57,11 +64,12 @@ class PhotoImagick extends PhotoDriver {
/*
* setup the compression here, so we'll do it only once
*/
switch($this->getType()) {
switch ($this->getType()) {
case 'image/png':
$quality = get_config('system', 'png_quality');
if((! $quality) || ($quality > 9))
if ((! $quality) || ($quality > 9)) {
$quality = PNG_QUALITY;
}
/*
* From http://www.imagemagick.org/script/command-line-options.php#quality:
*
@ -75,8 +83,9 @@ class PhotoImagick extends PhotoDriver {
break;
case 'image/jpeg':
$quality = get_config('system', 'jpeg_quality');
if((! $quality) || ($quality > 100))
if ((! $quality) || ($quality > 100)) {
$quality = JPEG_QUALITY;
}
$this->image->setCompressionQuality($quality);
default:
break;
@ -85,7 +94,7 @@ class PhotoImagick extends PhotoDriver {
}
protected function destroy() {
if($this->is_valid()) {
if ($this->is_valid()) {
$this->image->clear();
$this->image->destroy();
}
@ -108,7 +117,7 @@ class PhotoImagick extends PhotoDriver {
$this->image->stripImage();
if(! empty($profiles)) {
if (! empty($profiles)) {
$this->image->profileImage('icc', $profiles['icc']);
}
}
@ -122,8 +131,9 @@ class PhotoImagick extends PhotoDriver {
* @return boolean|\Imagick
*/
public function getImage() {
if(! $this->is_valid())
if (! $this->is_valid()) {
return false;
}
$this->image = $this->image->deconstructImages();
return $this->image;
@ -144,8 +154,9 @@ class PhotoImagick extends PhotoDriver {
}
public function rotate($degrees) {
if(! $this->is_valid())
if (! $this->is_valid()) {
return false;
}
$this->image->setFirstIterator();
do {
@ -157,8 +168,9 @@ class PhotoImagick extends PhotoDriver {
}
public function flip($horiz = true, $vert = false) {
if(! $this->is_valid())
if (! $this->is_valid()) {
return false;
}
$this->image->setFirstIterator();
do {
@ -170,8 +182,9 @@ class PhotoImagick extends PhotoDriver {
}
public function cropImageRect($maxx, $maxy, $x, $y, $w, $h) {
if(! $this->is_valid())
if (! $this->is_valid()) {
return false;
}
$this->image->setFirstIterator();
do {
@ -188,8 +201,9 @@ class PhotoImagick extends PhotoDriver {
}
public function imageString() {
if(! $this->is_valid())
if (! $this->is_valid()) {
return false;
}
/* Clean it */
$this->image = $this->image->deconstructImages();

View file

@ -48,7 +48,7 @@ require_once('include/items.php');
define ( 'STD_VERSION', '19.10.27' );
define ( 'STD_VERSION', '19.11.5' );
define ( 'ZOT_REVISION', '6.0' );
define ( 'DB_UPDATE_VERSION', 1236 );

View file

@ -1,11 +0,0 @@
Privacy Policy
==============
#include doc/gdpr1.md;
Terms of Service
================
#include doc/SiteTOS.md;

View file

@ -1,12 +1,18 @@
Privacy Policy
==============
Privacy Notice May 2018
The following applies mainly to account registrants on this site. You may use this website
without registering for an account.
How your information will be used
=================================
Information you provide to this website may be stored and used to provide services to you.
We require an email address to idenitfy the account holder. This will not be shared with
We require an email address to identify the account holder. This will not be shared with
any other website or service. It is used to send you notifications about your account and
perform administrative tasks such as resetting your password. You have the option to
opt-out of all email notifications through your settings.
@ -77,8 +83,7 @@ download for either archival puposes or to transfer to another compatible websit
Your rights
===========
Under the General Data Protection Regulation
(GDPR) and The Data Protection Act 2018
Under the General Data Protection Regulation (GDPR) and The Data Protection Act 2018
(DPA) you have a number of rights with regard to your personal data.
You have the right to request from us access to and rectification or erasure of your personal data,
the right to restrict processing, object to processing as well as in certain circumstances the right
@ -92,23 +97,24 @@ You have the right to lodge a complaint to the Information Commissioners Offi
believe that we have not complied with the requirements of the GDPR or DPA 18 with regard
to your personal data.
DMCA takedown notices (for illegal use of copyrighted information) MUST be initiated by the
owner of the copyright and should be addressed to the data protection officer, listed below.
Identity and contact details of controller and data protection officer
======================================================================
[NAME OF COMPANY]
is the controller
[and processor]
of data for the purposes of the DPA 18 and GDPR. 3
is the controller [and processor] of data for the purposes of the DPA section 18 and GDPR section 3.
If you have any concerns as to how your data is processed you can contact:
If you have any concerns as to how your data is processed you may contact:
[Website operator should include their contact details here].
Terms of Service
================
This website is physically located in [COUNTRY] and all usage is subject to the laws thereof.
[
[NAME]
Data Protection Offer at
[EMAIL ADDRESS]
]
[NAME] [JOB TITLE]
at
[EMAIL ADDRESS]
or you can write to these
individuals using the address of
[]

58
doc/en/groups.md Normal file
View file

@ -0,0 +1,58 @@
Groups
======
Groups facilitate communication on specific topics by providing a common area to interact.
Creating a Group
================
Open your profile menu. When using the standard/default theme, this is often done by clicking your photo in the top navigation bar. Select "Channels". On the Channel Management page click "Create New". For "Channel role and privacy" select one of the Community Group entries from the dropdown. Create a group name and nickname as desired and submit.
Group - Normal
This selection creates a public group where members are automatically approved by default. Group posts are shared publicly.
Group - Restricted
This selection creates a private group where members need to be approved to become members. Group posts are only shared with group members.
Group - Moderated
This selection creates a public group which requires posts and comments from new members to be moderated (approved by an administrator) prior to publishing. This can be disabled at any time for existing group members by editing the connection and deselecting the 'Moderated' permission for that channel.
Once the new channel is created you may setup the profile photo, cover photo and profile details as desired. To return to your original channel select "Channels" from the profile menu and select your original channel from the list provided. You may also wish to join (connect with) the group from your original channel as this does not happen automatically.
Joining a Group
===============
You join a group in the same way as you connect with any channel in the network. For public groups you may be automatically approved at the discretion of the group administrator.
Leaving a Group
===============
You leave a group by deleting the connection from within the Connections app.
Posting to a Group
==================
You can post to a group one of two ways.
1) Visit the group homepage using an authenticated link (this will usually be automatic when visiting pages inside the application) and post to the homepage creating a "wall-to-wall" post.
2) Send a DM (direct/private message) to the group. This will be re-published as a group post. A DM is created by creating a post and selecting the group as a private recipient from the access dialog (look for a lock button in the post editor). Alternatively you can create a post using "private mentions" and tag the group. A post with a private mention is only sent to those mentioned. A private mention begins with the characters @! followed by the group name. This should auto-complete as soon as you start typing the name.
When using a DM for group posting, please do not include any other recipients.
Group members using other software (Mastodon, MoodleNet, etc.) will need to use a DM to post to the group. The precise mechanism for sending a DM may vary depending on the software used.

5
doc/site/README.md Normal file
View file

@ -0,0 +1,5 @@
The site directory is for local modifications of documentation files.
Please copy the file doc/en/TermsOfService.md to doc/site/en/TermsOfService.md and add
the contact details for the site owner per the GDPR. You may also translate documentation
into other languages as desired.

View file

@ -2,44 +2,29 @@
use Michelf\MarkdownExtra;
/**
* @brief
*
* @param string $path
* @return string|unknown
*/
function get_help_fullpath($path,$suffix=null) {
$docroot = 'doc/';
return find_docfile($path,App::$language);
}
// Determine the language and modify the path accordingly
// $x = determine_help_language();
// $lang = $x['language'];
// $url_idx = ($x['from_url'] ? 1 : 0);
// The English translation is at the root of /doc/. Other languages are in
// subfolders named by the language code such as "de", "es", etc.
// if($lang !== 'en') {
// $langpath = $lang . '/' . $path;
// } else {
// $langpath = $path;
// }
function find_docfile($name,$language) {
$newpath = $docroot . $path;
if ($suffix) {
if (file_exists($newpath . $suffix)) {
return $newpath;
}
} elseif (file_exists($newpath . '.md') ||
file_exists($newpath . '.bb') ||
file_exists($newpath . '.html')) {
return $newpath;
}
return $newpath;
foreach([ $language, 'en' ] as $lang) {
if (file_exists('doc/site/' . $lang . '/' . $name . '.md')) {
return 'doc/site/' . $lang . '/' . $name . '.md';
}
if (file_exists('doc/' . $lang . '/' . $name . '.md')) {
return 'doc/' . $lang . '/' . $name . '.md';
}
}
return EMPTY_STR;
}
@ -56,27 +41,15 @@ function get_help_content($tocpath = false) {
$text = '';
$path = '';
$docroot = 'doc/';
$path = argv(1);
$fullpath = get_help_fullpath($path,'.md');
$fullpath = get_help_fullpath($path);
$text = load_doc_file($fullpath . '.md');
$text = load_doc_file($fullpath);
App::$page['title'] = t('Help');
if($doctype === 'markdown') {
# escape #include tags
$text = preg_replace('/#include/ism', '%%include', $text);
$content = MarkdownExtra::defaultTransform($text);
$content = preg_replace('/%%include/ism', '#include', $content);
}
$content = preg_replace_callback("/#include (.*?)\;/ism", 'preg_callback_help_include', $content);
$content = MarkdownExtra::defaultTransform($text);
return translate_projectname($content);
}

View file

@ -174,6 +174,27 @@ function collect_recipients($item, &$private_envelope,$include_groups = true) {
}
function comments_are_now_closed($item) {
$x = [
'item' => $item,
'closed' => 'unset'
];
/**
* @hooks comments_are_now_closed
* Called to determine whether commenting should be closed
* * \e array \b item
* * \e boolean \b closed - return value
*/
call_hooks('comments_are_now_closed', $x);
if ($x['closed'] != 'unset') {
return $x['closed'];
}
if($item['comments_closed'] > NULL_DATE) {
$d = datetime_convert();
if($d > $item['comments_closed'])

View file

@ -1633,7 +1633,7 @@ function get_site_info() {
}
$protocols = [ 'zot' ];
if (get_config('system','activitypub')) {
if (get_config('system','activitypub',true)) {
$protocols[] = 'activitypub';
}

View file

@ -1,28 +1,8 @@
<div id="help-content" class="generic-content-wrapper">
<div class="clearfix section-title-wrapper">
<div class="pull-right">
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-language" style="font-size: 1.4em;"></i>
</button>
<div class="dropdown-menu dropdown-menu-right flex-column lang-selector">
<a class="dropdown-item lang-choice" href="/help">de</a>
<a class="dropdown-item lang-choice" href="/help">en</a>
<a class="dropdown-item lang-choice" href="/help">es</a>
<a class="dropdown-item lang-choice" href="/help">fr</a>
</div>
</div>
</div>
<h2>{{$title}}: {{$heading}}</h2>
<h2>{{$title}} {{$heading}}</h2>
</div>
<div class="section-content-wrapper" id="doco-content">
<h3 id="doco-top-toc-heading">
<span class="fakelink" onclick="docoTocToggle(); return false;">
<i class="fa fa-fw fa-caret-right fakelink" id="doco-toc-toggle"></i>
{{$tocHeading}}
</span>
</h3>
<ul id="doco-top-toc" style="margin-bottom: 1.5em; display: none;"></ul>
{{$content}}
</div>
</div>

View file

@ -1,4 +1,4 @@
<div class="project-banner" title="{{$project_title}}"><a href="{{$baseurl}}"><img src="{{$project_icon}}" alt="{{$project_title}}"></a></div>
<div class="project-banner" title="{{$project_title}}"><a href="{{$baseurl}}/siteinfo"><img src="{{$project_icon}}" alt="{{$project_title}}"></a></div>
{{if $nav.login && !$userinfo}}
<div class="d-lg-none pt-1 pb-1">
{{if $nav.loginmenu.1.4}}

View file

@ -3,7 +3,7 @@
<h3>{{$sitenametxt}}</h3>
<div>{{$sitename}}</div>
<div><a href="{{$url}}">{{$sitename}}</a></div>
<h3>{{$headline}}</h3>