Merge branch 'dev' of codeberg.org:streams/streams into dev

This commit is contained in:
Mike Macgirvin 2023-09-12 14:02:59 +10:00
commit c6205c5d5f
39 changed files with 748 additions and 195 deletions

View file

@ -8,6 +8,42 @@ class Signature extends ASObject
public $creator;
public $signatureValue;
/**
* @return mixed
*/
public function getCreator()
{
return $this->creator;
}
/**
* @param mixed $creator
* @return Signature
*/
public function setCreator($creator)
{
$this->creator = $creator;
return $this;
}
/**
* @return mixed
*/
public function getSignatureValue()
{
return $this->signatureValue;
}
/**
* @param mixed $signatureValue
* @return Signature
*/
public function setSignatureValue($signatureValue)
{
$this->signatureValue = $signatureValue;
return $this;
}
/**
* @return mixed
*/

View file

@ -298,7 +298,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemLevel()
public function getLevel()
{
return $this->item_level;
}
@ -307,7 +307,7 @@ class Item extends BaseObject
* @param mixed $item_level
* @return Item
*/
public function setItemLevel($item_level)
public function setLevel($item_level)
{
$this->item_level = $item_level;
return $this;
@ -1072,7 +1072,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemRestrict()
public function getRestrict()
{
return $this->item_restrict;
}
@ -1081,7 +1081,7 @@ class Item extends BaseObject
* @param mixed $item_restrict
* @return Item
*/
public function setItemRestrict($item_restrict)
public function setRestrict($item_restrict)
{
$this->item_restrict = $item_restrict;
return $this;
@ -1090,7 +1090,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemFlags()
public function getFlags()
{
return $this->item_flags;
}
@ -1099,7 +1099,7 @@ class Item extends BaseObject
* @param mixed $item_flags
* @return Item
*/
public function setItemFlags($item_flags)
public function setFlags($item_flags)
{
$this->item_flags = $item_flags;
return $this;
@ -1108,7 +1108,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemPrivate()
public function getPrivate()
{
return $this->item_private;
}
@ -1117,7 +1117,7 @@ class Item extends BaseObject
* @param mixed $item_private
* @return Item
*/
public function setItemPrivate($item_private)
public function setPrivate($item_private)
{
$this->item_private = $item_private;
return $this;
@ -1126,7 +1126,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemOrigin()
public function getOrigin()
{
return $this->item_origin;
}
@ -1135,7 +1135,7 @@ class Item extends BaseObject
* @param mixed $item_origin
* @return Item
*/
public function setItemOrigin($item_origin)
public function setOrigin($item_origin)
{
$this->item_origin = $item_origin;
return $this;
@ -1144,7 +1144,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemUnseen()
public function getUnseen()
{
return $this->item_unseen;
}
@ -1153,7 +1153,7 @@ class Item extends BaseObject
* @param mixed $item_unseen
* @return Item
*/
public function setItemUnseen($item_unseen)
public function setUnseen($item_unseen)
{
$this->item_unseen = $item_unseen;
return $this;
@ -1162,7 +1162,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemStarred()
public function getStarred()
{
return $this->item_starred;
}
@ -1171,7 +1171,7 @@ class Item extends BaseObject
* @param mixed $item_starred
* @return Item
*/
public function setItemStarred($item_starred)
public function setStarred($item_starred)
{
$this->item_starred = $item_starred;
return $this;
@ -1180,7 +1180,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemUplink()
public function getUplink()
{
return $this->item_uplink;
}
@ -1189,7 +1189,7 @@ class Item extends BaseObject
* @param mixed $item_uplink
* @return Item
*/
public function setItemUplink($item_uplink)
public function setUplink($item_uplink)
{
$this->item_uplink = $item_uplink;
return $this;
@ -1198,7 +1198,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemConsensus()
public function getConsensus()
{
return $this->item_consensus;
}
@ -1207,7 +1207,7 @@ class Item extends BaseObject
* @param mixed $item_consensus
* @return Item
*/
public function setItemConsensus($item_consensus)
public function setConsensus($item_consensus)
{
$this->item_consensus = $item_consensus;
return $this;
@ -1216,7 +1216,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemWall()
public function getWall()
{
return $this->item_wall;
}
@ -1225,7 +1225,7 @@ class Item extends BaseObject
* @param mixed $item_wall
* @return Item
*/
public function setItemWall($item_wall)
public function setWall($item_wall)
{
$this->item_wall = $item_wall;
return $this;
@ -1234,7 +1234,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemThreadTop()
public function getThreadTop()
{
return $this->item_thread_top;
}
@ -1243,7 +1243,7 @@ class Item extends BaseObject
* @param mixed $item_thread_top
* @return Item
*/
public function setItemThreadTop($item_thread_top)
public function setThreadTop($item_thread_top)
{
$this->item_thread_top = $item_thread_top;
return $this;
@ -1252,7 +1252,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemNotshown()
public function getNotshown()
{
return $this->item_notshown;
}
@ -1261,7 +1261,7 @@ class Item extends BaseObject
* @param mixed $item_notshown
* @return Item
*/
public function setItemNotshown($item_notshown)
public function setNotshown($item_notshown)
{
$this->item_notshown = $item_notshown;
return $this;
@ -1270,7 +1270,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemNsfw()
public function getNsfw()
{
return $this->item_nsfw;
}
@ -1279,7 +1279,7 @@ class Item extends BaseObject
* @param mixed $item_nsfw
* @return Item
*/
public function setItemNsfw($item_nsfw)
public function setNsfw($item_nsfw)
{
$this->item_nsfw = $item_nsfw;
return $this;
@ -1288,7 +1288,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemRelay()
public function getRelay()
{
return $this->item_relay;
}
@ -1297,7 +1297,7 @@ class Item extends BaseObject
* @param mixed $item_relay
* @return Item
*/
public function setItemRelay($item_relay)
public function setRelay($item_relay)
{
$this->item_relay = $item_relay;
return $this;
@ -1306,7 +1306,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemMentionsme()
public function getMentionsme()
{
return $this->item_mentionsme;
}
@ -1315,7 +1315,7 @@ class Item extends BaseObject
* @param mixed $item_mentionsme
* @return Item
*/
public function setItemMentionsme($item_mentionsme)
public function setMentionsme($item_mentionsme)
{
$this->item_mentionsme = $item_mentionsme;
return $this;
@ -1324,7 +1324,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemNocomment()
public function getNocomment()
{
return $this->item_nocomment;
}
@ -1333,7 +1333,7 @@ class Item extends BaseObject
* @param mixed $item_nocomment
* @return Item
*/
public function setItemNocomment($item_nocomment)
public function setNocomment($item_nocomment)
{
$this->item_nocomment = $item_nocomment;
return $this;
@ -1342,7 +1342,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemObscured()
public function getObscured()
{
return $this->item_obscured;
}
@ -1351,7 +1351,7 @@ class Item extends BaseObject
* @param mixed $item_obscured
* @return Item
*/
public function setItemObscured($item_obscured)
public function setObscured($item_obscured)
{
$this->item_obscured = $item_obscured;
return $this;
@ -1360,7 +1360,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemVerified()
public function getVerified()
{
return $this->item_verified;
}
@ -1369,7 +1369,7 @@ class Item extends BaseObject
* @param mixed $item_verified
* @return Item
*/
public function setItemVerified($item_verified)
public function setVerified($item_verified)
{
$this->item_verified = $item_verified;
return $this;
@ -1378,7 +1378,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemRetained()
public function getRetained()
{
return $this->item_retained;
}
@ -1387,7 +1387,7 @@ class Item extends BaseObject
* @param mixed $item_retained
* @return Item
*/
public function setItemRetained($item_retained)
public function setRetained($item_retained)
{
$this->item_retained = $item_retained;
return $this;
@ -1396,7 +1396,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemRss()
public function getRss()
{
return $this->item_rss;
}
@ -1405,7 +1405,7 @@ class Item extends BaseObject
* @param mixed $item_rss
* @return Item
*/
public function setItemRss($item_rss)
public function setRss($item_rss)
{
$this->item_rss = $item_rss;
return $this;
@ -1414,7 +1414,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemDeleted()
public function getDeleted()
{
return $this->item_deleted;
}
@ -1423,7 +1423,7 @@ class Item extends BaseObject
* @param mixed $item_deleted
* @return Item
*/
public function setItemDeleted($item_deleted)
public function setDeleted($item_deleted)
{
$this->item_deleted = $item_deleted;
return $this;
@ -1432,7 +1432,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemType()
public function getType()
{
return $this->item_type;
}
@ -1441,7 +1441,7 @@ class Item extends BaseObject
* @param mixed $item_type
* @return Item
*/
public function setItemType($item_type)
public function setType($item_type)
{
$this->item_type = $item_type;
return $this;
@ -1450,7 +1450,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemHidden()
public function getHidden()
{
return $this->item_hidden;
}
@ -1459,7 +1459,7 @@ class Item extends BaseObject
* @param mixed $item_hidden
* @return Item
*/
public function setItemHidden($item_hidden)
public function setHidden($item_hidden)
{
$this->item_hidden = $item_hidden;
return $this;
@ -1468,7 +1468,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemUnpublished()
public function getUnpublished()
{
return $this->item_unpublished;
}
@ -1477,7 +1477,7 @@ class Item extends BaseObject
* @param mixed $item_unpublished
* @return Item
*/
public function setItemUnpublished($item_unpublished)
public function setUnpublished($item_unpublished)
{
$this->item_unpublished = $item_unpublished;
return $this;
@ -1486,7 +1486,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemDelayed()
public function getDelayed()
{
return $this->item_delayed;
}
@ -1495,7 +1495,7 @@ class Item extends BaseObject
* @param mixed $item_delayed
* @return Item
*/
public function setItemDelayed($item_delayed)
public function setDelayed($item_delayed)
{
$this->item_delayed = $item_delayed;
return $this;
@ -1504,7 +1504,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemPendingRemove()
public function getPendingRemove()
{
return $this->item_pending_remove;
}
@ -1513,7 +1513,7 @@ class Item extends BaseObject
* @param mixed $item_pending_remove
* @return Item
*/
public function setItemPendingRemove($item_pending_remove)
public function setPendingRemove($item_pending_remove)
{
$this->item_pending_remove = $item_pending_remove;
return $this;
@ -1522,7 +1522,7 @@ class Item extends BaseObject
/**
* @return mixed
*/
public function getItemBlocked()
public function getBlocked()
{
return $this->item_blocked;
}
@ -1531,7 +1531,7 @@ class Item extends BaseObject
* @param mixed $item_blocked
* @return Item
*/
public function setItemBlocked($item_blocked)
public function setBlocked($item_blocked)
{
$this->item_blocked = $item_blocked;
return $this;

View file

@ -3,7 +3,6 @@
namespace Code\Lib;
/**
* @file include/account.php
* @brief Some account related functions.
*/

View file

@ -8,6 +8,8 @@ use Code\ActivityStreams\Actor;
use Code\ActivityStreams\ASObject;
use Code\ActivityStreams\Link;
use Code\ActivityStreams\Place;
use Code\ActivityStreams\UnhandledElementException;
use Code\Nomad\Location;
use Code\Nomad\Profile;
use Code\Web\HTTPHeaders;
use Code\Web\HTTPSig;
@ -715,6 +717,7 @@ class Activity
}
if (array_key_exists('name', $att) && $att['name']) {
$entry['name'] = html2plain(purify_html($att['name']), 256);
$entry['name'] = str_replace('"', '"', $entry['name']);
}
// Friendica attachments don't match the URL in the body.
// This makes it more difficult to detect image duplication in bb_attach()
@ -1357,7 +1360,7 @@ class Activity
}
if ($activitypub && $has_images && $activity['type'] === 'Note') {
if ($activitypub && $has_images && in_array($activity['type'], ['Note', 'Story'])) {
foreach ($images as $match) {
$img = [];
// handle Friendica/Hubzilla style img links with [img=$url]$alttext[/img]
@ -1616,6 +1619,9 @@ class Activity
}
/**
* @throws UnhandledElementException
*/
public static function encode_person($p, $extended = true, $activitypub = false)
{
@ -1702,24 +1708,28 @@ class Activity
'oauthRegistrationEndpoint' => z_root() . '/api/client/register',
'oauthAuthorizationEndpoint' => z_root() . '/authorize',
'oauthTokenEndpoint' => z_root() . '/token',
'searchContent' => z_root() . '/search/' . $c['channel_address'] . '/?search={}',
'searchTags' => z_root() . '/search/' . $c['channel_address'] . '/?tag={}',
'searchContent' => z_root() . '/search/' . $c['channel_address'] . '?search={}',
'searchTags' => z_root() . '/search/' . $c['channel_address'] . '?tag={}',
];
$ret['discoverable'] = (bool)((1 - intval($p['xchan_hidden'])));
$searchPerm = PermissionLimits::Get($c['channel_id'], 'search_stream');
if ($searchPerm === PERMS_PUBLIC) {
$ret['canSearch'] = ACTIVITY_PUBLIC_INBOX;
$ret['indexable'] = true;
}
elseif (in_array($searchPerm, [ PERMS_SPECIFIC, PERMS_CONTACTS])) {
$ret['canSearch'] = z_root() . '/followers/' . $c['channel_address'];
$ret['indexable'] = false;
}
else {
$ret['canSearch'] = [];
$ret['indexable'] = false;
}
// force over-ride
if (Config::Get('system','block_public_search')) {
$ret['canSearch'] = [];
$ret['indexable'] = false;
}
@ -1737,6 +1747,7 @@ class Activity
// map other nomadic identities linked with this channel
$locations = [];
$nomadicLocations = [];
$locs = Libzot::encode_locations($c);
if ($locs) {
foreach ($locs as $loc) {
@ -1744,8 +1755,24 @@ class Activity
$locations[] = $loc['id_url'];
}
}
}
foreach ($locs as $loc) {
$loc = new Location($loc);
$sig = explode('.', $loc->getUrlSig(), 2);
$entry = [
'id' => $loc->getIdUrl(),
'url' => $loc->getIdUrl(),
'signature' => [
'id' => $loc->getIdUrl() . '?operation=getkey',
'nonce' => random_string(),
'creator' => $loc->getIdUrl(),
'signature' => base64_encode(Crypto::sign($loc->getIdUrl(), $c['channel_prvkey'])),
],
];
$nomadicLocations[] = $entry;
}
}
// $ret['nomadicLocations'] = $nomadicLocations;
if ($locations) {
if (count($locations) === 1) {
$locations = array_shift($locations);
@ -1754,6 +1781,8 @@ class Activity
$ret['alsoKnownAs'] = $locations;
}
// To move your followers from a Mastodon account,
// visit https://$yoursite/pconfig/system/movefrom
// And set the value to the URL of your Mastodon profile.
@ -2860,6 +2889,13 @@ class Activity
$item = [];
// Intransitives. Treat the target as the object in order to pick out any
// important fields and represent those as an item.
if (in_array($act->type,['Arrive','Leave']) && $act->tgt && !$act->obj) {
$act->obj = $act->tgt;
}
if (is_array($act->obj)) {
$binary = false;
$markdown = false;
@ -2947,6 +2983,13 @@ class Activity
} elseif ($act->objprop('expires')) {
$item['expires'] = datetime_convert('UTC', 'UTC', $act->obj['expires']);
}
// pixelfed stories
if (array_key_exists('expiresAt', $act->data) && $act->data['expiresAt']) {
$item['expires'] = datetime_convert('UTC', 'UTC', $act->data['expiresAt']);
} elseif ($act->objprop('expiresAt')) {
$item['expires'] = datetime_convert('UTC', 'UTC', $act->obj['expiresAt']);
}
if ($item['expires'] > NULL_DATE && $item['expires'] < datetime_convert()) {
// We shouldn't even be seeing this activity.
return false;
@ -3249,8 +3292,21 @@ class Activity
$item['app'] = escape_tags($generator['name']);
}
$location = (new Place($act->get_property_obj('location')));
if ($location->getType() === 'Place') {
if (is_array($act->tgt) && $act->tgt['type'] === 'Place') {
$location = new Place($act->tgt);
}
elseif (is_array($act->obj)) {
if ($act->obj['type'] === 'Place') {
$location = new Place($act->obj);
}
elseif (is_array($act->obj['location'])) {
$location = new Place($act->obj['location']);
}
}
else {
$location = new Place($act->get_property_obj('location'));
}
if ($location && $location->getType() === 'Place') {
$item['location'] = $location->getName() ? escape_tags($location->getName()) : '';
// Look for something resembling latitude/longitude coordinates in the place name and set the
// coordinates appropriately. This technically isn't supported but is provided as a convenience
@ -3301,7 +3357,7 @@ class Activity
// Objects that might have media attachments which aren't already provided in the content element.
// We'll check specific media objects separately.
if (in_array($act->objprop('type',''), ['Article', 'Document', 'Event', 'Note', 'Page', 'Place', 'Question'])
if (in_array($act->objprop('type',''), ['Article', 'Document', 'Event', 'Note', 'Story', 'Page', 'Place', 'Question'])
&& isset($item['attach']) && $item['attach']) {
$item = self::bb_attach($item);
}
@ -3492,7 +3548,7 @@ class Activity
}
if (in_array($act->objprop('type'), ['Note', 'Article', 'Page'])) {
if (in_array($act->objprop('type'), ['Note', 'Story', 'Article', 'Page'])) {
$ptr = null;
if (array_key_exists('url', $act->obj)) {
@ -4764,9 +4820,11 @@ class Activity
'alsoKnownAs' => 'as:alsoKnownAs',
'EmojiReact' => 'as:EmojiReact',
'discoverable' => 'toot:discoverable',
'indexable' => 'toot:indexable',
'wall' => 'sm:wall',
'capabilities' => 'litepub:capabilities',
'acceptsJoins' => 'litepub:acceptsJoins',
'nomadicLocations' => 'nomad:nomadicLocations',
'Hashtag' => 'as:Hashtag',
'canReply' => 'toot:canReply',
'approval' => 'toot:approval',

View file

@ -529,7 +529,7 @@ class Libsync
$found = false;
if ($abook['abook_xchan'] && $abook['xchan_addr'] && (!in_array($abook['xchan_network'], ['token', 'unknown']))) {
$h = Libzot::get_hublocs($abook['abook_xchan']);
$h = Libzot::getLocations($abook['abook_xchan']);
if ($h) {
$found = true;
} else {

View file

@ -71,7 +71,7 @@ class Libzot
/**
* @brief Given a Nomad hash, return all distinct hubs.
*
* This function is used in building the zot discovery packet and therefore
* This function is used in building the Nomad discovery packet and therefore
* should only be used by channels which are defined on this hub.
*
* @param string $hash - xchan_hash
@ -79,13 +79,13 @@ class Libzot
*
*/
public static function get_hublocs($hash)
public static function getLocations($hash)
{
/* Only search for active hublocs - e.g. those that haven't been marked deleted */
$ret = q(
"select * from hubloc where hubloc_hash = '%s' and hubloc_deleted = 0 order by hubloc_url ",
"select * from hubloc where hubloc_hash = '%s' and hubloc_deleted = 0 order by hubloc_primary DESC, hubloc_url ",
dbesc($hash)
);
@ -2521,14 +2521,14 @@ class Libzot
* @param array $channel an associative array which must contain
* * \e string \b channel_hash the hash of the channel
* @return array an array with associative arrays
* @see self::get_hublocs()
* @see self::getLocations()
*/
public static function encode_locations($channel)
{
$ret = [];
$x = self::get_hublocs($channel['channel_hash']);
$x = self::getLocations($channel['channel_hash']);
if ($x && count($x)) {
foreach ($x as $hub) {

View file

@ -118,7 +118,7 @@ class ThreadItem
$locktype = intval($item['item_private']);
$shareable = ((($conv->get_profile_owner() == local_channel() && local_channel()) && (! intval($item['item_private']))) ? true : false);
$shareable = ($conv->get_profile_owner() == local_channel() && local_channel()) && (! intval($item['item_private']));
$repeatable = false;
// allow an exemption for sharing stuff from your private feeds

View file

@ -28,6 +28,9 @@ class Security
$use_fep5624 = ((x($_POST, 'use_fep5624')) ? intval($_POST['use_fep5624']) : 0);
set_config('system', 'use_fep5624', $use_fep5624);
$require_authenticated_fetch = ((x($_POST, 'require_authenticated_fetch')) ? 1 : 0);
set_config('system', 'require_authenticated_fetch', $require_authenticated_fetch);
$accept_unsigned_relay = ((x($_POST, 'accept_unsigned_relay')) ? 1 : 0);
set_config('system', 'accept_unsigned_relay', $accept_unsigned_relay);
@ -145,6 +148,7 @@ class Security
'$title' => t('Administration'),
'$page' => t('Security'),
'$form_security_token' => get_form_security_token('admin_security'),
'$require_authenticated_fetch' => ['require_authenticated_fetch', t('Require signed fetch requests'), Config::Get('system','require_authenticated_fetch'), ''],
'$accept_unsigned_relay' => ['accept_unsigned_relay', t('Accept unsigned relayed activities'), Config::Get('system','accept_unsigned_relay'),''],
'$block_public_search' => array('block_public_search', t("Block public search"), get_config('system', 'block_public_search', 1), t("Prevent access to search content unless you are currently authenticated.")),
'$block_public_dir' => ['block_public_directory', t('Block directory from visitors'), get_config('system', 'block_public_directory', true), t('Only allow authenticated access to directory.')],

View file

@ -2,6 +2,8 @@
namespace Code\Module;
use Code\Lib\Config;
use Code\Lib\LibBlock;
use Code\Lib\Libzot;
use Code\Lib\Activity;
use Code\Lib\Libprofile;
@ -131,16 +133,49 @@ class Channel extends Controller
if (intval($channel['channel_system'])) {
goaway(z_root());
}
$sigdata = HTTPSig::verify(EMPTY_STR);
if ($sigdata['portable_id'] && $sigdata['header_valid']) {
$portable_id = $sigdata['portable_id'];
if (!check_channelallowed($portable_id)) {
http_status_exit(403, 'Permission denied');
}
if (!check_siteallowed($sigdata['signer'])) {
http_status_exit(403, 'Permission denied');
}
if (LibBlock::fetch_by_entity($channel['channel_id'],$sigdata['signer'])
|| LibBlock::fetch_by_entity($channel['channel_id'],$sigdata['portable_id'])) {
http_status_exit(403, 'Permission denied');
}
observer_auth($portable_id);
}
elseif (Config::Get('system', 'require_authenticated_fetch', false)) {
http_status_exit(403, 'Permission denied');
}
as_return_and_die(Activity::encode_person($channel, true, true), $channel);
}
// handle zot6 channel discovery
if (Libzot::is_nomad_request()) {
$sigdata = HTTPSig::verify(file_get_contents('php://input'), EMPTY_STR, 'zot6');
$sigdata = HTTPSig::verify(($_SERVER['REQUEST_METHOD'] === 'POST') ? file_get_contents('php://input') : '', EMPTY_STR, 'zot6');
if ($sigdata && $sigdata['signer'] && $sigdata['header_valid']) {
$portable_id = $sigdata['portable_id'];
if (!check_channelallowed($portable_id)) {
http_status_exit(403, 'Permission denied');
}
if (!check_siteallowed($sigdata['signer'])) {
http_status_exit(403, 'Permission denied');
}
if (LibBlock::fetch_by_entity($channel['channel_id'],$sigdata['signer'])
|| LibBlock::fetch_by_entity($channel['channel_id'],$sigdata['portable_id'])) {
http_status_exit(403, 'Permission denied');
}
$data = json_encode(Libzot::zotinfo(['guid_hash' => $channel['channel_hash'], 'target_url' => $sigdata['signer']]));
$s = q(
"select site_crypto, hubloc_sitekey from site left join hubloc on hubloc_url = site_url where hubloc_id_url = '%s' and hubloc_network in ('nomad','zot6') and hubloc_deleted = 0 order by hubloc_id desc limit 1",
@ -151,6 +186,9 @@ class Channel extends Controller
$data = json_encode(Crypto::encapsulate($data, $s[0]['hubloc_sitekey'], Libzot::best_algorithm($s[0]['site_crypto'])));
}
} else {
if (Config::Get('system', 'require_authenticated_fetch', false)) {
http_status_exit(403, 'Permission denied');
}
$data = json_encode(Libzot::zotinfo(['guid_hash' => $channel['channel_hash']]));
}

View file

@ -43,6 +43,7 @@ class Connections extends Controller
$active = false;
$blocked = false;
$moderated = false;
$hidden = false;
$ignored = false;
$archived = false;
@ -70,6 +71,13 @@ class Connections extends Controller
$head = t('Blocked');
$blocked = true;
break;
case 'moderated':
$search_flags = "and xchan_hash in (select xchan from abconfig where chan = "
. local_channel()
. " and cat = 'system' and k = 'my_perms' and v like '%%moderated%%')";
$head = t('Moderated');
$moderated = true;
break;
case 'ignored':
$search_flags = " and abook_ignored = 1 ";
$head = t('Ignored');
@ -160,6 +168,13 @@ class Connections extends Controller
'title' => t('Only show blocked connections'),
],
'moderated' => [
'label' => t('Moderated'),
'url' => z_root() . '/connections/moderated',
'sel' => ($moderated) ? 'active' : '',
'title' => t('Only show moderated connections'),
],
'ignored' => [
'label' => t('Ignored'),
'url' => z_root() . '/connections/ignored',

View file

@ -24,7 +24,6 @@ use Code\Render\Theme;
use Code\Lib\Url;
/**
* @file connedit.php
* @brief In this file the connection-editor form is generated and evaluated.
*/
@ -488,6 +487,8 @@ class Connedit extends Controller
case 'hide':
$flag_result = abook_toggle_flag($orig_record, ABOOK_FLAG_HIDDEN);
break;
case 'moderate':
$flag_result = abook_toggle_permission($orig_record, 'moderated');
case 'approve':
if (intval($orig_record['abook_pending'])) {
$flag_result = abook_toggle_flag($orig_record, ABOOK_FLAG_PENDING);
@ -596,6 +597,14 @@ class Connedit extends Controller
'info' => (intval($contact['abook_blocked']) ? t('This connection is blocked') : ''),
],
'moderate' => [
'label' => (my_perms_contains(intval($contact['abook_channel']), $contact['abook_xchan'], 'moderated') ? t('Unmoderate') : t('Moderate')),
'url' => z_root() . '/connedit/' . $contact['abook_id'] . '/moderate',
'sel' => (my_perms_contains(intval($contact['abook_channel']), $contact['abook_xchan'], 'moderated') ? 'active' : ''),
'title' => t('Moderate (or Unmoderate) all communications with this connection'),
'info' => (my_perms_contains(intval($contact['abook_channel']), $contact['abook_xchan'], 'moderated') ? t('This connection is moderated') : ''),
],
'ignore' => [
'label' => (intval($contact['abook_ignored']) ? t('Unignore') : t('Ignore')),
'url' => z_root() . '/connedit/' . $contact['abook_id'] . '/ignore',

View file

@ -95,6 +95,9 @@ class Dav extends Controller
}
}
}
else {
logger('No authentication headers found. Ignore this message for the very first request.');
}
if (!is_dir('store')) {
Stdio::mkdir('store', STORAGE_DEFAULT_PERMISSIONS, false);

View file

@ -108,7 +108,11 @@ class Editpost extends Controller
if ($j) {
foreach ($j as $jj) {
if (!str_starts_with($jj['type'],'application/ld+json')) {
$item['body'] .= "\n" . '[attachment]' . basename($jj['href']) . ',' . $jj['revision'] . '[/attachment]' . "\n";
$r = q("select hash from attach where display_path = '%s'",
dbesc(str_replace(z_root() . '/cloud/' . $channel['channel_address'] . '/','',$jj['href']))
);
$hash = $r ? $r[0]['hash'] : $jj['href'];
$item['body'] .= "\n" . '[attachment]' . $hash . ',' . $jj['revision'] . '[/attachment]' . "\n";
}
}
}

View file

@ -88,7 +88,7 @@ class Inbox extends Controller
if (!$AS->is_valid()) {
if ($AS->deleted) {
// process mastodon user deletion activities, but only if we can validate the signature
if ($hsig['header_valid'] && $hsig['content_valid'] && $hsig['portable_id']) {
if ($hsig['portable_id']) {
logger('removing deleted actor');
remove_all_xchan_resources($hsig['portable_id']);
} else {
@ -124,7 +124,6 @@ class Inbox extends Controller
// if the sender has the ability to send messages over zot/nomad, ignore messages sent via activitypub
// as observer aware features and client side markup will be unavailable
/** @noinspection PhpRedundantOptionalArgumentInspection */
$test = Activity::get_actor_hublocs($hsig['portable_id'], 'all,not_deleted');
if ($test) {
foreach ($test as $t) {
@ -136,7 +135,7 @@ class Inbox extends Controller
// fetch the portable_id for the actor, which may or may not be the sender
$v = Activity::get_actor_hublocs($AS->actor['id'], 'activitypub,not_deleted');
$v = Activity::get_actor_hublocs($AS->actor['id']);
if ($v && $v[0]['hubloc_hash'] !== $hsig['portable_id']) {
// The sender is not actually the activity actor, so verify the LD signature.
@ -154,7 +153,6 @@ class Inbox extends Controller
}
}
if ($v) {
// The sender has been validated and stored
$observer_hash = $hsig['portable_id'];
@ -297,6 +295,9 @@ class Inbox extends Controller
return;
}
foreach ($channels as $channel) {
logger('delivery_channel: ' . $channel['channel_address']);
}
// Bto and Bcc will only be present in a C2S transaction and should not be stored.
$saved_recips = [];
@ -308,6 +309,7 @@ class Inbox extends Controller
$AS->set_recips($saved_recips);
foreach ($channels as $channel) {
// Even though activitypub may be enabled for the site, check if the channel has specifically disabled it
if (!PConfig::Get($channel['channel_id'], 'system', 'activitypub', Config::Get('system', 'activitypub', ACTIVITYPUB_ENABLED))) {

View file

@ -1195,7 +1195,13 @@ class Item extends Controller
if (in_array($verb, ['Arrive', 'Leave'])) {
$body = preg_replace('/\[map=(.*?)\]/','', $body);
$body = preg_replace('/\[map\](.*?)\[\/map\]/','', $body);
$datarray['tgt_type'] = 'Place';
$datarray['target'] = [
'type' => 'Place',
'name' => $location ?? '',
'latitude' => $lat,
'longitude' => $lon,
];
if ($lat || $lon) {
$body .= "\n\n" . '[map=' . $lat . ',' . $lon . ']' . "\n";
}

View file

@ -36,7 +36,7 @@ class Opensearch extends Controller {
$html = replace_macros(Theme::get_template('opensearch.tpl'), [
'$project' => xmlify($channel['channel_address'] . '@' . App::get_hostname()),
'$search_project' => xmlify(t('Search $Projectname') . '|' . $channel['channel_address'] . '@' . App::get_hostname()),
'$searchurl' => xmlify(z_root() . '/search/' . $channel['channel_address'] . '?search={searchTerms}'),
'$searchurl' => xmlify(zid(z_root() . '/search/' . $channel['channel_address'] . '?search={searchTerms}')),
'$aptype' => xmlify('application/ld+json; profile="https://www.w3.org/ns/activitystreams"'),
'$photo' => xmlify($channel['xchan_photo_s']),
]);
@ -45,9 +45,9 @@ class Opensearch extends Controller {
$html = replace_macros(Theme::get_template('opensearch.tpl'), [
'$project' => xmlify(App::get_hostname()),
'$search_project' => xmlify(t('Search $Projectname') . '|' . App::get_hostname()),
'$searchurl' => z_root() . '/search?search={searchTerms}',
'$searchurl' => xmlify(zid(z_root() . '/search?search={searchTerms}')),
'$aptype' => xmlify('application/ld+json; profile="https://www.w3.org/ns/activitystreams"'),
'$photo' => $icon,
'$photo' => xmlify($icon),
]);
}
echo $html;

View file

@ -3,24 +3,23 @@
namespace Code\Module;
use App;
use Code\Lib\Libprofile;
use Code\Lib\Libzot;
use Code\Web\Controller;
use Code\Daemon\Run;
use Code\Lib\Activity;
use Code\Lib\ActivityStreams;
use Code\Lib\ASCollection;
use Code\Lib\Queue;
use Code\Daemon\Run;
use Code\Lib\Channel;
use Code\Lib\Navbar;
use Code\Render\Theme;
use Code\Lib\LDSignatures;
use Code\Lib\Libprofile;
use Code\Lib\Libzot;
use Code\Lib\Navbar;
use Code\Lib\Queue;
use Code\Render\Theme;
use Code\Web\Controller;
use Code\Web\HTTPSig;
require_once("include/bbcode.php");
require_once('include/security.php');
require_once('include/conversation.php');
require_once('include/security.php');
class Search extends Controller
{
@ -34,10 +33,12 @@ class Search extends Controller
public $mintags = 0;
public $search_channel = null;
public $search;
public function init()
{
if (x($_REQUEST, 'search')) {
App::$data['search'] = escape_tags($_REQUEST['search']);
$this->search = escape_tags($_REQUEST['search']);
}
}
@ -119,11 +120,13 @@ class Search extends Controller
$output .= '<div class="generic-content-wrapper-styled">' . "\r\n";
$output .= '<h3>' . t('Search') . '</h3>';
if (!empty(App::$data['search'])) {
$search = trim(App::$data['search']);
if (!empty($this->search)) {
$search = trim($this->search);
} else {
$search = ((x($_GET, 'search')) ? trim(escape_tags(rawurldecode($_GET['search']))) : '');
$search = $this->search = ((x($_GET, 'search')) ? trim(escape_tags(rawurldecode($_GET['search']))) : '');
}
$saved_id = 'search=' . urlencode($_GET['search']);
$tag = false;
if (x($_GET, 'tag')) {
@ -197,7 +200,6 @@ class Search extends Controller
$regstr = db_getfunc('REGEXP');
$sql_extra = sprintf(" AND (item.title $regstr '%s' OR item.body $regstr '%s') ", dbesc(protect_sprintf(preg_quote($search))), dbesc(protect_sprintf(preg_quote($search))));
}
}
else {
if ($format === '') {
@ -214,8 +216,8 @@ class Search extends Controller
// because browser prefetching might change it on us. We have to deliver it with the page.
$output .= '<div id="live-search"></div>' . "\r\n";
$output .= "<script> var profile_uid = " . intval($this->search_channel['channel_id'])
. "; var netargs = '?f='; var profile_page = " . App::$pager['page'] . "; divmore_height = 400; </script>\r\n";
$output .= "<script> let profile_uid = " . intval($this->search_channel['channel_id'])
. "; let netargs = '?f='; let profile_page = " . App::$pager['page'] . "; divmore_height = 400; </script>\r\n";
App::$page['htmlhead'] .= replace_macros(Theme::get_template("build_query.tpl"), [
'$baseurl' => z_root(),

View file

@ -113,7 +113,7 @@ class Share extends Controller
$arr['uuid'] = new_uuid();
$arr['mid'] = z_root() . '/item/' . $arr['uuid'];
$arr['mid'] = str_replace('/item/', '/activity/', $arr['mid']);
$arr['parent_mid'] = $item['mid'];
$arr['parent_mid'] = $arr['mid'];
$mention = '@[zrl=' . $item['author']['xchan_url'] . ']' . $item['author']['xchan_name'] . '[/zrl]';
$arr['body'] = sprintf(t('&#x1f501; Repeated %1$s\'s %2$s'), $mention, $item['obj_type']);

View file

@ -6,29 +6,123 @@ use Code\Lib\BaseObject;
class Site extends BaseObject
{
/*
* @param string
* public facing url of site
*/
public $url;
/*
* @param signature
* site url signed with site private key prepended with method and period
* eg: sha256.op007a8iiOSuXUkqsjkcLf8h75LVnf2Df71fXKjTTakaZN7bEZ9ADjjNJKY9AYsKHKHrl3OLjCBBxFxytTMzGfd4Vq-TMVXsGlR
*/
public $site_sig;
/*
* @param string
* url of Nomad post endpoint
*/
public $post;
/*
* @param string
* url of OpenWebAuth endpoint
*/
public $openWebAuth;
/*
* @param string
* url of "reverse" OpenWebAuth endpoint
*/
public $authRedirect;
/*
* @param string
* site public key PEM
*/
public $sitekey;
/*
* @param array
* array of supported transport encryption algorithms
*/
public $encryption;
/*
* @param string
* default signature algorithm transmitted as hs2019
* draft-cavage-http-signatures
* example: sha256 for rsa-sha256
*/
public $signature_algorithm;
/*
* @param string
* current Nomad protocol version
* example: 12.0
*/
public $protocol_version;
/*
* @param string enum
* options: 'approve', 'open', 'closed'
*
*/
public $register_policy;
/*
* @param string enum
* options: 'paid', 'free', 'tiered', 'private'
*/
public $access_policy;
/*
* @param string
* admin email address
*/
public $admin;
/*
* @param text/x-multicode
* description of site
*/
public $about;
/*
* @param string
* random string generated during site install to detect duplicate installations
*/
public $sitehash;
/*
* @param string
* URL of site marketing page to direct potential new members
* if registration is not closed
*/
public $sellpage;
/*
* @param string
* site location text when choosing sites
* example: 'California, USA'
*/
public $location;
/*
* @param string
* The human-readable name of this site
*/
public $sitename;
/*
* @param string
* URL of site logo/avatar (300x300 preferred)
*/
public $logo;
/*
* deprecated: replaced with community
*/
public $project;
/*
* @param string
* Version of the software
* Example: 23.01.31
*/
public $version;
/*
* param string
* Name of this server community
* default: transliterated $sitename
* example: 'pirates' for sitename of 'Pirates of Penzance'
*
*/
public $community;
// deprecated
// deprecated. Replaced by protocol_version
public $zot;

View file

@ -132,7 +132,7 @@ class PhotoImagick extends PhotoDriver
/**
* @brief Return a \Imagick object of the current image.
* @brief Return an Imagick object from the current image.
*
* @see \Code\Photo\PhotoDriver::getImage()
*

View file

@ -107,7 +107,7 @@ class GitRepo
public function setRepoPath($directory)
{
if (is_dir($directory)) {
$this->path->$directory;
$this->path = $directory;
$this->git->setRepository($directory);
return true;
}
@ -126,6 +126,7 @@ class GitRepo
if (validate_url($this->url) && $this->isValidGitRepoURL($this->url) && is_dir($this->path)) {
return $this->git->clone($this->url, $this->path);
}
return false;
}
public function probeRepo()

View file

@ -108,7 +108,7 @@ class Settings_menu implements WidgetInterface
dbesc($channel['channel_hash'])
);
$sources = q("select * from sources where src_channel_id = %d",
$sources = q("select * from source where src_channel_id = %d",
intval($channel['channel_id'])
);

View file

@ -1,7 +1,22 @@
Federation
==========
The ActivityPub implementation in this project strives to be compliant to the core spec where possible, while offering a range of services and features which normally aren't provided by ActivityPub projects.
The ActivityPub implementation in this software strives to be compliant to the core spec where possible, while offering a range of services and features which normally aren't provided by ActivityPub projects.
Supported activities:
- `Follow(Actor)`, `Accept(Follow)`, `Reject(Follow)`, `Undo(Follow)`.
- `Join(Group)`, `Accept(Join)`, `Reject(Join)`, `Leave(Group)`
- `Create(Note|Article|Question|Page|Document|Image|Video|Audio)`, `Update(Note|Article|Question|Page|Document|Image|Video|Audio)`, `Delete(Note|Article|Question|Page|Document|Image|Video|Audio)`.
- `Like()`, `Undo(Like)`.
- `Dislike()`,`Undo(Dislike)`.
- `Invite(Event)`, `Accept(Invite)`, `Reject(Invite)`, `TentativeAccept(Invite)`, `TentativeReject(Invite)`, `Update(Event)`, `Delete(Event)`.
- `Arrive(target: Place)`, `Leave(target: Place)`
- `Arrive(Place|Note+location)`, `Leave(Place|Note+location)`.
- `Announce(Note)`, `Undo(Announce)`.
- `Update(Actor)`, `Move(Actor)`, `Delete(Actor)`.
Many other activity combinations we've seen in the wild are partially supported. If you're looking for something specific which is not in this list, please ask (https://unfediverse.com/channel/streams).
C2S
@ -11,11 +26,19 @@ Client search interface
If public access is allowed to the content search interface (a site security setting), clients may search the content of public messages or tags and are returned an ActivityStreams Collection of search results. When authenticated via OpenWebAuth, the search results may contain their own content or private content which they are permitted to access.
The URL endpoints are:
The URL endpoints for site search are:
https://example.com/search?search=banana
https://example.com/search?tag=banana
The URL endpoints for individual channel search are:
https://example.com/search/username?search=banana
https://example.com/search/username?tag=banana
Search permission is indicated by the 'canSearch' attribute of the actor record. This is an array of actors or groups or access lists that are allowed to search the given channel.
Direct Messages
@ -23,7 +46,7 @@ Direct Messages (DM) are differentiated from other private messaging using the n
Events
Events and RSVP are supported per AS-vocabulary with the exception that a Create/Event is not transmitted. Invite/Event is the primary method of sharing events. For compatibiliity with some legacy applications, RSVP responses of Accept/Event, Reject/Event, TentativeAccept/Event and TentativeReject/Event are accepted as valid RSVP activities. By default we send Accept/{Invite/Event} (and other RSVP responses) per AS-vocabulary. Events with no timezone (e.g. "all day events" or holidays) are sent with no 'Z' on the event times per RFC3339 Section 4.3. All event times are in UTC and timezone adjusted events are transmitted using Zulu time 'yyyy-mm-ddThh:mm:ssZ'. Event descriptions are sent using 'summary' and accepted using summary, title, and content in order of existence. These are converted internally to plaintext if they contain HTML. If 'location' contains coordinates, a map will typically be generated when rendered.
Events and RSVP are supported per AS-vocabulary with the exception that a Create/Event is not transmitted. Invite/Event is the primary method of sharing events. For compatibility with some legacy applications, RSVP responses of Accept/Event, Reject/Event, TentativeAccept/Event and TentativeReject/Event are accepted as valid RSVP activities. By default, we send Accept/{Invite/Event} (and other RSVP responses) per AS-vocabulary. Events with no timezone (e.g. "all day events" or holidays) are sent with no 'Z' on the event times per RFC3339 Section 4.3. All event times are in UTC and timezone adjusted events are transmitted using Zulu time 'yyyy-mm-ddThh:mm:ssZ'. Event descriptions are sent using 'summary' and accepted using summary, title, and content in order of existence. These are converted internally to plaintext if they contain HTML. If 'location' contains coordinates, a map will typically be generated when rendered.
Nomadic Identity
@ -44,7 +67,7 @@ Update: as of 2021-04-30 We will support incoming group posts of the form Create
Comments
This project provides permission control and moderation of comments. By default comments are only accepted from existing connections. This can be changed by the individual. Other sites MAY use zot:commentPolicy (string) as a guide if they do not wish to provide comment abilities where it is known in advance they will be rejected. A Reject/Note activity will be sent if the comment is not permitted. There is currently no response for moderated content, but will likely also be represented by Reject/Note.
This project provides permission control and moderation of comments. By default comments are only accepted from existing connections. This can be changed by the individual. Other sites MAY use nomad:commentPolicy (string) as a guide if they do not wish to provide comment abilities where it is known in advance they will be rejected. A Reject/Note activity will be sent if the comment is not permitted. There is currently no response for moderated content, but will likely also be represented by Reject/Note.
'commentPolicy' can be any of
@ -62,10 +85,11 @@ This project provides permission control and moderation of comments. By default
'until=2001-01-01T00:00Z' - comments are closed after the date given. This can be supplied on its own or appended to any other commentPolicy string by preceding with a space; for example 'contacts until=2001-01-01T00:00Z'.
The 'canReply' field may also be used for comment control. This contains either an actor id, or an array of actor ids; which may include groups or other actor collections such as access lists.
Expiring content
Activity objects may include an 'expires' field; after which time they are removed. The removal occurs with a federated Delete, but this is a best faith effort. We automatically delete any local objects we receive with an 'exires' field after it expires regardless of whether or not we receive a Delete activity. We also record external (3rd party) fetches of these items and send Delete activities to them as well. The expiration is specified as an ISO8601 date/time.
Activity objects may include an 'expires' field; after which time they are removed. The removal occurs with a federated Delete, but this is a best faith effort. We automatically delete any local objects we receive with an 'exires' field after it expires regardless of whether or not we receive a Delete activity. The expiration is specified as an ISO8601 date/time.
Private Media
@ -118,8 +142,8 @@ Our projects send comment notifications if somebody replies to a post you either
Conversation Completion
(2021-04-17) It's easy to fetch missing pieces of a conversation going "upstream", but there is no agreed-on method to fetch a complete conversation from the viewpoint of the origin actor and upstream fetching only provides a single conversation branch, rather than the entire tree. We provide 'context' as a URL to a collection containing the entire conversation (all known branches) as seen by its creator. This requires special treatment and is very similar to ostatus:conversation in that if context is present, it needs to be replicated in conversation descendants. We still support ostatus:conversation but usage is deprecated. We do not use 'replies' to achieve the same purposes because 'replies' only applies to direct descendants at any point in the conversation tree.
(2021-04-17) It's easy to fetch missing pieces of a conversation going "upstream", but there is no agreed-on method to fetch a complete conversation from the viewpoint of the origin actor and upstream fetching only provides a single conversation branch, rather than the entire tree. We provide both 'context' and 'audience' as a URL to a collection containing the entire conversation (all known branches) as seen by its creator. This requires special treatment and is very similar to ostatus:conversation in that if context is present, it needs to be replicated in conversation descendants. We still support ostatus:conversation but usage is deprecated. We do not use 'replies' to achieve the same purposes because 'replies' only applies to direct descendants at any point in the conversation tree.
Site Actors
(2021-08-25) An actor record of type 'Service' is now available from an ActivityStreams fetch of the domain root. This was discussed recently in the Socialhub as it may open some novel applications which involve communication with sites and site administators; and also provides a simple ActivityPub centric method for discovering very basic information about a site that doesn't involve platform-centric APIS. At present this is just a skeleton which will be filled in as we better define the ways we see it being used.
(2021-08-25) An actor record of type 'Service' is now available from an ActivityStreams fetch of the domain root. This was discussed recently in the Socialhub as it may open some novel applications which involve communication with sites and site administators; and also provides a simple ActivityPub centric method for discovering very basic information about a site that doesn't involve platform-centric APIs. At present this is just a skeleton which will be filled in as we better define the ways we see it being used.

View file

@ -1,6 +1,6 @@
An open source fediverse server with a long history of innovation. See [FEATURES](https://codeberg.org/streams/streams/src/branch/dev/FEATURES.md).
This software is dedicated to the public domain to the extent permissable by law and is not associated with any consumer brand or product.
This software is dedicated to the public domain to the extent permissible by law and is not associated with any consumer brand or product.
This repository uses a community-driven model. This means that there are no dedicated developers working on new features or bug fixes or translations or documentation. Instead, it relies on the contributed efforts of those that choose to use it.

View file

@ -8,30 +8,202 @@ Previously this was a site security setting made by the site admin.
## Implementation Details
The first part of this epoch was to enable the fetching of search results as an ActivityStreams 'Collection'. This work was completed some time ago.
Search results are available either via the web or via ActivityPub endpoints. There are two endpoints available. The first is at [https://example.com]/search and searches the entire site. The second is an individual channel search at [https://example.com]/search/[username] . This only searches an individual channel.
The second phase is providing a new permission named 'search_stream'. Only viewers possessing both 'view_stream' and 'search_stream' permission can search your channel content going forward. This permission can be assigned individually, or automatically by channel Permission Role and the personal Roles app.
Access is controlled by a permission named 'search_stream'. Only viewers possessing both 'view_stream' and 'search_stream' permission can search your channel content. This permission can be assigned individually, or automatically by the channel Permission Role and the personal Roles app.
The underlying search module is also to be changed so that the URL may contain a channel name. In the absence of a channel username in the URL the entire site will be searched, but will only provide results from channels which have permitted search to the current viewer. When a channel username is present, the search is restricted to content authored by that channel.
The site search is an aggregate of all channels that allow the viewer to search their content. The search results may include protected or private content which is permitted to be seen by the viewer. If the viewer cannot be determined, only public content is returned.
The channel search URL will be provided in the ActivityPub and Nomad actor records in the optional 'endpoints' section. The names used are 'nomad:searchContent' and 'nomad:searchTags'. These endpoints will be essentially a template, containing a bracket pair {} which is then substituted with the desired search text. If evaluated with the nomad: prefix, the URL is guaranteed to provide results for both HTML and ActivityPub/ActivityStreams/Nomad/Zot6 requests. If used with any other namespace prefix, it is only assumed to provide an ActivityPub compliant endpoint.
The search permission is available to federated protocols via an attribute 'canSearch' in the actor record. This consists of an id or array consisting of the actor ids of those who are permitted to search content. Usually this will be an access list or the ActivityPub 'followers' collection. It may also contain individual actor ids or the ActivityPub "public inbox" identifier if public searching is allowed. An empty entry means nobody can search the channel.
If a channel has federated search enabled, a new entry will be provided in the author action drop down attached to every post/comment. This will be named 'Search' (or 'Search Content' and 'Search Tags') and will perform an HTML search of the provided endpoint(s). This entry will only be present if the provided endpoints are associated with the 'nomad:' context namespace.
If a channel has opted out of channel discovery (inclusion in federated directories), no search links will be provided; with the exception of the so-called 'system channel' which represents the site. The system channel will provide these links purely based on the system setting of 'block_public_search'.
## Discovery
## Acceptance Criteria and Testing
The channel search URL is provided in the ActivityPub and Nomad actor records in the optional 'endpoints' section. The names used are 'nomad:searchContent' and 'nomad:searchTags'. These endpoints will be essentially a template, containing a bracket pair {} which is then substituted with the desired search text. This URL provides results for both HTML and ActivityPub/ActivityStreams/Nomad/Zot6 requests.
Validate the existence of the endpoint across all protocols when the discovery criteria have been met.
Example ActivityPub actor record with 'canSearch' attribute and search endpoints
Validate that a site search does not contain content created by a member which you do not have permission to search.
```
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"nomad": "https://macgirvin.com/apschema#",
"toot": "http://joinmastodon.org/ns#",
"litepub": "http://litepub.social/ns#",
"sm": "http://smithereen.software/ns#",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"oauthRegistrationEndpoint": "litepub:oauthRegistrationEndpoint",
"sensitive": "as:sensitive",
"movedTo": "as:movedTo",
"alsoKnownAs": "as:alsoKnownAs",
"EmojiReact": "as:EmojiReact",
"discoverable": "toot:discoverable",
"wall": "sm:wall",
"capabilities": "litepub:capabilities",
"acceptsJoins": "litepub:acceptsJoins",
"Hashtag": "as:Hashtag",
"canReply": "toot:canReply",
"approval": "toot:approval",
"isContainedConversation": "nomad:isContainedConversation",
"conversation": "nomad:conversation",
"commentPolicy": "nomad:commentPolicy",
"eventRepeat": "nomad:eventRepeat",
"emojiReaction": "nomad:emojiReaction",
"expires": "nomad:expires",
"directMessage": "nomad:directMessage",
"Category": "nomad:Category",
"replyTo": "nomad:replyTo",
"copiedTo": "nomad:copiedTo",
"canSearch": "nomad:canSearch",
"searchContent": "nomad:searchContent",
"searchTags": "nomad:searchTags"
}
],
"type": "Person",
"id": "https://macgirvin.com/channel/mike",
"preferredUsername": "mike",
"name": "Mike Macgirvin",
"updated": "2023-07-07T09:24:00Z",
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
"updated": "2022-11-25T22:10:44Z",
"url": "https://macgirvin.com/photo/profile/l/5",
"height": 300,
"width": 300
},
"url": "https://macgirvin.com/channel/mike",
"location": {
"type": "Place",
"name": "Bugger All, Australia"
},
"tag": [
{
"type": "Note",
"name": "Protocol",
"content": "zot6"
},
{
"type": "Note",
"name": "Protocol",
"content": "nomad"
},
{
"type": "Note",
"name": "Protocol",
"content": "activitypub"
}
],
"inbox": "https://macgirvin.com/inbox/mike",
"outbox": "https://macgirvin.com/outbox/mike",
"followers": "https://macgirvin.com/followers/mike",
"following": "https://macgirvin.com/following/mike",
"wall": "https://macgirvin.com/outbox/mike",
"endpoints": {
"sharedInbox": "https://macgirvin.com/inbox",
"oauthRegistrationEndpoint": "https://macgirvin.com/api/client/register",
"oauthAuthorizationEndpoint": "https://macgirvin.com/authorize",
"oauthTokenEndpoint": "https://macgirvin.com/token",
"searchContent": "https://macgirvin.com/search/mike/?search={}",
"searchTags": "https://macgirvin.com/search/mike/?tag={}"
},
"discoverable": true,
"canSearch": "https://macgirvin.com/followers/mike",
"publicKey": {
"id": "https://macgirvin.com/channel/mike?operation=getkey",
"owner": "https://macgirvin.com/channel/mike",
"signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoaTPG5iYKsHJ1cK5CJUy
iN2y6B7aI4JkKMjjZO0gy8+6oa5kx6H5w7qED937a/SvwuYxh1A5yO9nwoEarM5s
PoYZ+Z+GGbAcvzdWURtDDdRMNgktAayDiOvEdiEbgPVx8f39YnpX39ngM8ukob16
S8eNwjWG6uwBs6rxSA409fkWjjbQwbe8fNOpynFWoG8jrB+dK6huryYqkyf9S18u
01IAJOo1ErtaUNkSzkeudXSWokRbN/P77N8LQXorwPF9U6ODblX9QXCWl6EnQ0CX
fcC/3NM6uXfda2KTn83G1+mo5QgGYBjGzE9K1VngoyX4J8AqvQxoXkqV20vwFSqW
ccB13F5kqRQ4BoQm2v2/e65YzjrHwkUecj7tS8TVXu8z4mdbDDbso/UrS14JmrJh
jnbwPOYpHX/6p2SdYDTF/vUWUmeSatS7sHK92eTRukuONig+PNvx8GUtxgMWPIgH
jIupGnR5lZxFMP+iaAmfxOSbVNeLn/Nka7+UfkDThApfhesBA6P8jAdStTCyqAYB
Y3rTTwplcaKKnNv+pLqBqyhYEghmGvv2EC2UGsL6rFit1RaZgSXWCIcLzdRZo4Ak
znvz8+juMjyPLp7DdSHhKss9kV9HDxZXjrstDxOR61j0vifaMh6bUVrOAMm0Ffs+
41v+D6pSA5p0OI2aqNJzLZ8CAwEAAQ==
-----END PUBLIC KEY-----
"
},
"manuallyApprovesFollowers": true,
"image": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://macgirvin.com/photo/aea3d61f-5f31-4572-8249-dd01e70ab7d8-7"
},
"summary": "Farmer in Bugger All, Australia. Plays a mean guitar, upside down and backwards. Systems integration, software development, and robot repair.",
"attachment": [
{
"type": "Note",
"name": "birthday",
"content": "0000-00-00"
}
],
"signature": {
"type": "RsaSignature2017",
"nonce": "8f848b54a05750904e60fa7a770f130c6ec9c19b9c5a29bdd2ebd83e4e679575",
"creator": "https://macgirvin.com/channel/mike",
"created": "2023-08-29T20:37:56Z",
"signatureValue": "PcNuQ7sZ5qHk1UIi0UsBlHdm4AolT6iEnVWfGmi292Is7A2vFVEMutmWb7y/OUy9b0kLq9o2ScAMbv3vkmKQcyWmk/qez0koMWoHmsxMcYWJE7rxnZWrulMSomRO0qNGgX+D2TsLRsGd5kktC/PgRbqD4OKzvV9jTgGnGaEqb1CYTQp9X/w66zG/jV+y94NTIe60iG8xVsLtLZRw/Y+LCu06BKfvDajQit9WSNVZVnqCub8IR63SxFtyWNZuolGpzjVW7z3C86Tj3Sv89lonf5XtR6JEUniFHZokbL4Ie23fbK4IKVAu5AriBYlMK2hMcpxKxFfHgOjav7B9yxze4QV10LjxGHZUklgzJwQUBX92RmZIpu5Cdsw94kNLnq1Xy5b6Xyam8TiPBOgEnUiZ74zDi3e9WKqxvTp2dsgC3g+sCCSQ5u6388nLzhREQWEwAlvhQ5p3M1gpg6P0S9HFA9syVjP0g2tlE3jK1RfUY/1sIp7awb0GdstN5iVF8BHDmd+uYaTZxfKw/cZ+Wa6IOVlAD0wSZnlvsms/DvPeMOcn7Pd+TxiJm4flSqFdkEDABc5WTFikHDw6O3KZTbhyeXKLyWNxTch/kwCo+6Fi6S3Wj38IkP7spuuw+VFxetIo3n44oGXzu4YuBII3ZbUG/Kp9V+OVHP5t+hs4qTWhvMQ="
}
}
```
Validate that a channel search only contains content created by the requested channel and that matching results are found if content exists containing that search text.
A similar record is provided for the site and is located at the site or domain root. It is identical, except that the site search presents an aggregated search, and can also be separately controlled by a site security option "Block Public Search".
Repeat results testing with hashtag search.
If a channel has federated search enabled, an entry is provided in the author action drop down attached to every post/comment. This performs an HTML search of the provided endpoint(s).
Verify the search results contain the same information whether fetched via HTML or via federated protocol and that the returned data objects are valid for the federated protocol used.
## Opensearch
## Data Migration
Search endpoint discovery is also provided by Opensearch. The opensearch definition is provided in webfinger for either a channel or for the site (identified by the actor record for the site/domain root).
Example webfinger entry at domain root:
```
{
"rel": "http://a9.com/-/spec/opensearch/1.1/",
"type": "application/opensearchdescription+xml",
"href": "https://macgirvin.com/opensearch",
"title": "macgirvin.com"
}
```
Example webfinger entry for an individual actor:
```
{
"rel": "http://a9.com/-/spec/opensearch/1.1/",
"type": "application/opensearchdescription+xml",
"href": "https://macgirvin.com/opensearch/mike",
"title": "mike@macgirvin.com"
}
```
This links to the following opensearch document:
```
<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>mike@macgirvin.com</ShortName>
<Description>Search Zap|mike@macgirvin.com</Description>
<Image height="64" width="64" type="image/png">https://macgirvin.com/photo/profile/s/5</Image>
<Url type="text/html" template="https://macgirvin.com/search/mike?search={searchTerms}"/>
<Url type="application/ld+json; profile=&quot;https://www.w3.org/ns/activitystreams&quot;" template="https://macgirvin.com/search/mike?search={searchTerms}"/>
</OpenSearchDescription>
```
The second Url in the example is a pointer to the ActivityPub enabled endpoint. A similar document is provided for the site search. Since an HTML Url is provided, these searches will be available as a browser search engine from many browsers and are also available to any opensearch enabled service. These services will only search public content (if public searches are permitted) as there is currently no mechanism for opensearch to determine the identity of the viewer. There are plans to integrate this with [OpenWebAuth](https://codeberg.org/streams/streams/src/branch/dev/spec/OpenWebAuth/Home.md), so this restriction is temporary.
The opensearch links are also provided as links in the actor's channel page. Both the channel search and the site search are listed.
```
<link rel="search" href="https://macgirvin.com/opensearch" type="application/opensearchdescription+xml" title="Search zap (macgirvin.com)" />
<link rel="search" href="https://macgirvin.com/opensearch/mike" type="application/opensearchdescription+xml" title="mike@macgirvin.com" />
```
A database migration may be required to set the initial 'search_stream' permission correctly for existing channels based on the channel permissions role.

View file

@ -264,7 +264,26 @@ function abook_toggle_flag($abook, $flag)
return $r;
}
function abook_toggle_permission($abook,$permission)
{
$perms = [];
$x = get_abconfig($abook['abook_channel'], $abook['abook_xchan'], 'system', 'my_perms');
if ($x) {
$y = explode(',', $x);
if (!in_array($permission, $y)) {
$perms = $y;
$perms[] = $permission;
} else {
foreach ($y as $z) {
if ($z !== $permission) {
$perms[] = $z;
}
}
}
}
set_abconfig($abook['abook_channel'], $abook['abook_xchan'], 'system', 'my_perms', implode( ',', $perms));
return true;
}
/**
* mark any hubs "offline" that haven't been heard from in more than 30 days

View file

@ -45,8 +45,6 @@ function xchan_store_lowlevel($arr)
function xchan_store($arr)
{
$update_photo = false;
$update_name = false;
@ -62,7 +60,6 @@ function xchan_store($arr)
if (! $arr['hash']) {
return false;
}
$r = q(
"select * from xchan where xchan_hash = '%s' limit 1",
dbesc($arr['hash'])

View file

@ -2,11 +2,11 @@ INPUT = README.md index.php boot.php include/ install/ util/ view/ Code/
RECURSIVE = YES
PROJECT_NAME = "Streams"
PROJECT_LOGO = images/streams-64.png
EXCLUDE = .htconfig.php library/ doc/ store/ vendor/ .git/ util/generate-hooks-index/
EXCLUDE = .htconfig.php cache/ library/ doc/ store/ vendor/ .git/ util/generate-hooks-index/
EXCLUDE_PATTERNS = *smarty3* *strings.php *.out *test*
OUTPUT_DIRECTORY = doc
OUTPUT_DIRECTORY = ./
GENERATE_HTML = YES
HTML_OUTPUT = html/
HTML_OUTPUT = streams/
HTML_FILE_EXTENSION = .html
GENERATE_LATEX = NO
EXTRACT_ALL = YES

8
util/doxygen Executable file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
# generates doxygen source code documentatiin
# Too use, sudo apt install doxygen
# or your package manager equivalent
doxygen util/Doxyfile
echo open the following link in your browser
echo file://`pwd`/streams/index.html

27
util/pconfig.md Normal file
View file

@ -0,0 +1,27 @@
CLI pconfig utility
==================
Usage:
pconfig
displays all available channels and their ids
pconfig {{uid}}
displays all pconfig entries for {{uid}}
pconfig {{uid}} family
displays all pconfig entries for family (system, database, etc)
pconfig {{uid}} family key
displays single pconfig entry for specified family and key
pconfig {{uid}} family key value
set pconfig entry for specified family and key to value and display result
NOTES:
family and key may be entered in dot notation, example:
pconfig 14 system.activitypub_enabled
is the same as
pconfig 14 system activitypub_enabled

View file

@ -1,2 +1,2 @@
<?php
define ('STD_VERSION', '23.08.26');
define ('STD_VERSION', '23.09.09');

View file

@ -42,7 +42,7 @@
}
#jot-title {
width: 90%;
width: 88%;
}
.comment-reset {
margin-right: 0.5rem;
@ -216,7 +216,7 @@ a.wall-item-name-link {
margin-bottom: 20px;
}
.ivoted {
.wall-item-tools i.ivoted {
color: #007bff;
}

View file

@ -8,7 +8,7 @@ function contact_search(term, callback, backend_url, type, extra_channels, spine
$(spinelement).show();
}
var postdata = {
let postdata = {
start:0,
count:100,
search:term,
@ -24,7 +24,7 @@ function contact_search(term, callback, backend_url, type, extra_channels, spine
data: postdata,
dataType: 'json',
success: function(data) {
var items = data.items.slice(0);
let items = data.items.slice(0);
items.unshift({taggable:false, text: term, replace: term});
callback(items);
$(spinelement).hide();
@ -37,7 +37,7 @@ contact_search.cache = {};
function contact_format(item) {
// Show contact information if not explicitly told to show something else
if(typeof item.text === 'undefined') {
var desc = ((item.label) ? item.nick + ' ' + item.label : item.nick);
let desc = ((item.label) ? item.nick + ' ' + item.label : item.nick);
if(typeof desc === 'undefined') desc = '';
if(desc) desc = ' ('+desc+')';
return "<div class='{0} dropdown-item dropdown-notification clearfix' title='{4}'><img class='menu-img-2' src='{1}' loading='lazy'><span class='font-weight-bold contactname'>{2}</span><span class='dropdown-sub-text'>{4}</span></div>".format(item.taggable, item.photo, item.name, desc, typeof(item.link) !== 'undefined' ? item.link : desc.replace('(','').replace(')',''));
@ -90,18 +90,18 @@ function trim_replace(item) {
}
function getWord(text, caretPos) {
var index = text.indexOf(caretPos);
var postText = text.substring(caretPos, caretPos+13);
let index = text.indexOf(caretPos);
let postText = text.substring(caretPos, caretPos+13);
if (postText.indexOf('[/list]') > 0 || postText.indexOf('[/checklist]') > 0 || postText.indexOf('[/ul]') > 0 || postText.indexOf('[/ol]') > 0 || postText.indexOf('[/dl]') > 0) {
return postText;
}
}
function getCaretPosition(ctrl) {
var CaretPos = 0; // IE Support
let CaretPos = 0; // IE Support
if (document.selection) {
ctrl.focus();
var Sel = document.selection.createRange();
let Sel = document.selection.createRange();
Sel.moveStart('character', -ctrl.value.length);
CaretPos = Sel.text.length;
}
@ -117,7 +117,7 @@ function setCaretPosition(ctrl, pos){
ctrl.setSelectionRange(pos,pos);
}
else if (ctrl.createTextRange) {
var range = ctrl.createTextRange();
let range = ctrl.createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos);
@ -126,15 +126,15 @@ function setCaretPosition(ctrl, pos){
}
function listNewLineAutocomplete(id) {
var text = document.getElementById(id);
var caretPos = getCaretPosition(text)
var word = getWord(text.value, caretPos);
let text = document.getElementById(id);
let caretPos = getCaretPosition(text)
let word = getWord(text.value, caretPos);
if (word != null) {
var textBefore = text.value.substring(0, caretPos);
var textAfter = text.value.substring(caretPos, text.length);
var textInsert = (word.indexOf('[/dl]') > 0) ? '\r\n[*=] ' : (word.indexOf('[/checklist]') > 0) ? '\r\n[] ' : '\r\n[*] ';
var caretPositionDiff = (word.indexOf('[/dl]') > 0) ? 3 : 1;
let textBefore = text.value.substring(0, caretPos);
let textAfter = text.value.substring(caretPos, text.length);
let textInsert = (word.indexOf('[/dl]') > 0) ? '\r\n[*=] ' : (word.indexOf('[/checklist]') > 0) ? '\r\n[] ' : '\r\n[*] ';
let caretPositionDiff = (word.indexOf('[/dl]') > 0) ? 3 : 1;
$('#' + id).val(textBefore + textInsert + textAfter);
setCaretPosition(text, caretPos + (textInsert.length - caretPositionDiff));
@ -212,12 +212,12 @@ function string2bb(element) {
};
var Textarea = Textcomplete.editors.Textarea;
let Textarea = Textcomplete.editors.Textarea;
$(this).each(function() {
var editor = new Textarea(this);
var textcomplete = new Textcomplete(editor);
textcomplete.register([contacts,groups,smilies,tags], {className:'acpopup', zIndex:1020});
let editor = new Textarea(this);
let textcomplete = new Textcomplete(editor);
textcomplete.register([contacts,groups,smilies,tags], {className:'acpopup', zIndex:1050});
});
@ -264,13 +264,13 @@ function string2bb(element) {
template: tag_format
};
var textcomplete;
var Textarea = Textcomplete.editors.Textarea;
let textcomplete;
let Textarea = Textcomplete.editors.Textarea;
$(this).each(function() {
var editor = new Textarea(this);
let editor = new Textarea(this);
textcomplete = new Textcomplete(editor);
textcomplete.register([contacts,tags], {className:'acpopup', maxCount:100, zIndex: 1020, appendTo:'nav'});
textcomplete.register([contacts,tags], {className:'acpopup', maxCount:100, zIndex: 1050, appendTo:'nav'});
});
textcomplete.on('selected', function() { this.editor.el.form.submit(); });
@ -297,20 +297,20 @@ function string2bb(element) {
template: contact_format,
};
var textcomplete;
var Textarea = Textcomplete.editors.Textarea;
let textcomplete;
let Textarea = Textcomplete.editors.Textarea;
$(this).each(function() {
var editor = new Textarea(this);
let editor = new Textarea(this);
textcomplete = new Textcomplete(editor);
textcomplete.register([contacts], {className:'acpopup', zIndex:1020});
textcomplete.register([contacts], {className:'acpopup', zIndex:1050});
});
if(autosubmit)
textcomplete.on('selected', function() { this.editor.el.form.submit(); });
if(typeof onselect !== 'undefined')
textcomplete.on('select', function() { var item = this.dropdown.getActiveItem(); onselect(item.searchResult.data); });
textcomplete.on('select', function() { let item = this.dropdown.getActiveItem(); onselect(item.searchResult.data); });
};
})( jQuery );
@ -333,20 +333,20 @@ function string2bb(element) {
template: contact_format,
};
var textcomplete;
var Textarea = Textcomplete.editors.Textarea;
let textcomplete;
let Textarea = Textcomplete.editors.Textarea;
$(this).each(function() {
var editor = new Textarea(this);
let editor = new Textarea(this);
textcomplete = new Textcomplete(editor);
textcomplete.register([contacts], {className:'acpopup', zIndex:1020});
textcomplete.register([contacts], {className:'acpopup', zIndex:1050});
});
if(autosubmit)
textcomplete.on('selected', function() { this.editor.el.form.submit(); });
if(typeof onselect !== 'undefined')
textcomplete.on('select', function() { var item = this.dropdown.getActiveItem(); onselect(item.searchResult.data); });
textcomplete.on('select', function() { let item = this.dropdown.getActiveItem(); onselect(item.searchResult.data); });
};
})( jQuery );
@ -371,20 +371,20 @@ function string2bb(element) {
};
var textcomplete;
var Textarea = Textcomplete.editors.Textarea;
let textcomplete;
let Textarea = Textcomplete.editors.Textarea;
$(this).each(function() {
var editor = new Textarea(this);
let editor = new Textarea(this);
textcomplete = new Textcomplete(editor);
textcomplete.register([names], {className:'acpopup', zIndex:1020});
textcomplete.register([names], {className:'acpopup', zIndex:1050});
});
if(autosubmit)
textcomplete.on('selected', function() { this.editor.el.form.submit(); });
if(typeof onselect !== 'undefined')
textcomplete.on('select', function() { var item = this.dropdown.getActiveItem(); onselect(item.searchResult.data); });
textcomplete.on('select', function() { let item = this.dropdown.getActiveItem(); onselect(item.searchResult.data); });
};
})( jQuery );
@ -396,24 +396,24 @@ function string2bb(element) {
return;
if(type=='bbcode') {
var open_close_elements = ['bold', 'italic', 'underline', 'overline', 'strike', 'superscript', 'subscript', 'quote', 'code', 'open', 'spoiler', 'map', 'nobb', 'list', 'checklist', 'ul', 'ol', 'dl', 'li', 'table', 'tr', 'th', 'td', 'center', 'color', 'font', 'size', 'zrl', 'zmg', 'rpost', 'question', 'answer', 'observer', 'observer.language','embed', 'highlight', 'url', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
var open_elements = ['observer.baseurl', 'observer.address', 'observer.photo', 'observer.name', 'observer.webname', 'observer.url', '*', 'hr', ];
let open_close_elements = ['bold', 'italic', 'underline', 'overline', 'strike', 'superscript', 'subscript', 'quote', 'code', 'open', 'spoiler', 'map', 'nobb', 'list', 'checklist', 'ul', 'ol', 'dl', 'li', 'table', 'tr', 'th', 'td', 'center', 'color', 'font', 'size', 'zrl', 'zmg', 'rpost', 'question', 'answer', 'observer', 'observer.language','embed', 'highlight', 'url', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
let open_elements = ['observer.baseurl', 'observer.address', 'observer.photo', 'observer.name', 'observer.webname', 'observer.url', '*', 'hr', ];
var elements = open_close_elements.concat(open_elements);
let elements = open_close_elements.concat(open_elements);
}
if(type=='comanche') {
var open_close_elements = ['region', 'layout', 'template', 'theme', 'widget', 'block', 'menu', 'var', 'css', 'js', 'authored', 'comment', 'webpage'];
var open_elements = [];
let open_close_elements = ['region', 'layout', 'template', 'theme', 'widget', 'block', 'menu', 'var', 'css', 'js', 'authored', 'comment', 'webpage'];
let open_elements = [];
var elements = open_close_elements.concat(open_elements);
let elements = open_close_elements.concat(open_elements);
}
if(type=='comanche-block') {
var open_close_elements = ['menu', 'var'];
var open_elements = [];
let open_close_elements = ['menu', 'var'];
let open_elements = [];
var elements = open_close_elements.concat(open_elements);
let elements = open_close_elements.concat(open_elements);
}
bbco = {
@ -452,17 +452,17 @@ function string2bb(element) {
};
var Textarea = Textcomplete.editors.Textarea;
let Textarea = Textcomplete.editors.Textarea;
$(this).each(function() {
var editor = new Textarea(this);
var textcomplete = new Textcomplete(editor);
textcomplete.register([bbco], {className:'acpopup', zIndex:1020});
let editor = new Textarea(this);
let textcomplete = new Textcomplete(editor);
textcomplete.register([bbco], {className:'acpopup', zIndex:1050});
});
this.keypress(function(e){
if (e.keyCode == 13) {
var x = listNewLineAutocomplete(this.id);
let x = listNewLineAutocomplete(this.id);
if(x) {
e.stopImmediatePropagation();
e.preventDefault();

View file

@ -11,6 +11,32 @@ html {
--bs-font-sans-serif: "Liberation Sans",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji" !important;
}
.wall-item-tools i {
color: #4d4d4d;
}
.wall-item-tools i:hover {
color: #f8f8f8;
}
.sys-apps-toggle {
}
.btn {
border: none;
font-size: 1.1rem;
margin-right: 0.4rem;
}
.generic-icons-nav {
font-size: 1.1rem;
}
.nav-link {
font-size: 1.1rem;
}
.dropdown-item, .nav-item {
font-size: 1.1rem;
}
.project-banner {
float: left;
margin-right: 1.5rem;
@ -1674,6 +1700,11 @@ blockquote {
z-index:1030;
}
.navbar-dark {
scrollbar-color: black white;
}
.navbar-dark .navbar-nav .nav-link,
.usermenu i {
color: $nav_icon_colour;
@ -1682,7 +1713,8 @@ blockquote {
.navbar-dark .navbar-nav .nav-link:focus,
.navbar-dark .navbar-nav .nav-link:hover,
.usermenu:focus i,
.usermenu:hover i {
.usermenu:hover i,
.sys-apps-toggle {
color: $nav_active_icon_colour;
}

View file

@ -13,6 +13,7 @@
<a class="dropdown-item" href="#" title="{{$tools.rephoto.title}}" onclick="window.location.href='{{$tools.rephoto.url}}'; return false;">{{$tools.rephoto.label}}</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" title="{{$tools.block.title}}" onclick="window.location.href='{{$tools.block.url}}'; return false;">{{$tools.block.label}}</a>
<a class="dropdown-item" href="#" title="{{$tools.moderate.title}}" onclick="window.location.href='{{$tools.moderate.url}}'; return false;">{{$tools.moderate.label}}</a>
<a class="dropdown-item" href="#" title="{{$tools.ignore.title}}" onclick="window.location.href='{{$tools.ignore.url}}'; return false;">{{$tools.ignore.label}}</a>
<a class="dropdown-item" href="#" title="{{$tools.censor.title}}" onclick="window.location.href='{{$tools.censor.url}}'; return false;">{{$tools.censor.label}}</a>
<a class="dropdown-item" href="#" title="{{$tools.archive.title}}" onclick="window.location.href='{{$tools.archive.url}}'; return false;">{{$tools.archive.label}}</a> <a class="dropdown-item" href="#" title="{{$tools.hide.title}}" onclick="window.location.href='{{$tools.hide.url}}'; return false;">{{$tools.hide.label}}</a>

View file

@ -7,6 +7,7 @@
{{include file="field_checkbox.tpl" field=$use_hs2019}}
{{include file="field_checkbox.tpl" field=$use_fep5624}}
{{include file="field_checkbox.tpl" field=$require_authenticated_fetch}}
{{include file="field_checkbox.tpl" field=$accept_unsigned_relay}}
{{include file="field_checkbox.tpl" field=$block_public_search}}
{{include file="field_checkbox.tpl" field=$block_public_dir}}

View file

@ -49,7 +49,7 @@
{{if $item.thread_author_menu}}
<i class="fa fa-caret-down wall-item-photo-caret cursor-pointer" data-bs-toggle="dropdown"></i>
<div class="dropdown-menu">
<img src="{{$item.large_avatar}}" style="width: 200px; height: 200px;" id="wall-item-popup-photo-{{$item.id}}" alt="{{$item.name}}" />
<img src="{{$item.large_avatar}}" style="width: 200px; height: 200px; margin: 10px;" id="wall-item-popup-photo-{{$item.id}}" alt="{{$item.name}}" />
<div style="margin-top: 20px;">
<hr>
{{foreach $item.thread_author_menu as $mitem}}

View file

@ -104,7 +104,7 @@
</div>
</div>
<div id="profile-jot-submit-wrapper" class="clearfix jothidden p-2">
<div id="profile-jot-submit-left" class="btn-toolbar float-start">
<div id="profile-jot-submit-left" class="btn-toolbar float-start" style="max-width: 50%;">
{{if $bbcode && $feature_markup}}
<div id="jot-markup" class="btn-group mr-2 ">
<button id="main-editor-bold" class="btn btn-outline-secondary btn-sm" title="{{$bold}}" onclick="inserteditortag('b', 'profile-jot-text'); return false;">
@ -250,11 +250,7 @@
</div>
<div id="profile-jot-submit-right" class="btn-group float-end">
<div class="btn-group ">
{{if $preview}}
<button class="btn btn-outline-secondary btn-sm" onclick="preview_post();return false;" title="{{$preview}}">
<i class="fa fa-eye jot-icons" ></i>
</button>
{{/if}}
{{if $save}}
<button class="btn btn-sm{{if $is_draft}} btn-warning{{else}} btn-outline-secondary{{/if}}" onclick="save_draft();return false;" title="{{$save}}">
<i class="fa fa-floppy-o jot-icons" ></i>
@ -280,6 +276,11 @@
<i id="jot-perms-icon" class="fa fa-{{$lockstate}} jot-icons{{if $bang}} jot-lock-warn{{/if}}"></i>
</button>
{{/if}}
{{if $preview}}
<button class="btn btn-outline-secondary btn-sm" onclick="preview_post();return false;" title="{{$preview}}">
<i class="fa fa-eye jot-icons" ></i>
</button>
{{/if}}
<button id="dbtn-submit" class="btn btn-primary btn-sm" type="submit" tabindex="3" name="button-submit">{{$share}}</button>
</div>
</div>

View file

@ -177,7 +177,7 @@
{{foreach $channel_apps as $channel_app}}
{{$channel_app|replace:'dropdown-item':'nav-link'}}
{{/foreach}}
<div class="dropdown-header text-white-50 sys-apps-toggle" onclick="openClose('sys-apps-collapsed');">
<div class="dropdown-header sys-apps-toggle" onclick="openClose('sys-apps-collapsed');">
{{$sysapps_toggle}}
</div>
<div id="sys-apps-collapsed" style="display:none;">