it's done

This commit is contained in:
nobody 2022-02-15 20:08:28 -08:00
parent 9bd7cd2a5e
commit a8e96322f0
1430 changed files with 125648 additions and 125648 deletions

View file

@ -0,0 +1,169 @@
<?php
namespace Code\Access;
/**
* @brief AccessControl class which represents individual content ACLs.
*
* A class to hold an AccessControl object with allowed and denied contacts and
* groups.
*
* After evaluating @ref ::Code::Access::PermissionLimits "PermissionLimits"
* and @ref ::Code::Lib::Permcat "Permcat"s individual content ACLs are evaluated.
* These answer the question "Can Joe view *this* album/photo?".
*/
class AccessControl
{
/**
* @brief Allow contacts
* @var string
*/
private $allow_cid;
/**
* @brief Allow groups
* @var string
*/
private $allow_gid;
/**
* @brief Deny contacts
* @var string
*/
private $deny_cid;
/**
* @brief Deny groups
* @var string
*/
private $deny_gid;
/**
* @brief Indicates if we are using the default constructor values or
* values that have been set explicitly.
* @var bool
*/
private $explicit;
/**
* @brief Constructor for AccessList class.
*
* @note The array to pass to the constructor is different from the array
* that you provide to the set() or set_from_array() functions.
*
* @param array $channel A channel array, where these entries are evaluated:
* * \e string \b channel_allow_cid => string of allowed cids
* * \e string \b channel_allow_gid => string of allowed gids
* * \e string \b channel_deny_cid => string of denied cids
* * \e string \b channel_deny_gid => string of denied gids
*/
public function __construct($channel)
{
if ($channel) {
$this->allow_cid = $channel['channel_allow_cid'];
$this->allow_gid = $channel['channel_allow_gid'];
$this->deny_cid = $channel['channel_deny_cid'];
$this->deny_gid = $channel['channel_deny_gid'];
} else {
$this->allow_cid = '';
$this->allow_gid = '';
$this->deny_cid = '';
$this->deny_gid = '';
}
$this->explicit = false;
}
/**
* @brief Get if we are using the default constructor values
* or values that have been set explicitly.
*
* @return bool
*/
public function get_explicit()
{
return $this->explicit;
}
/**
* @brief Set access list from strings such as those in already
* existing stored data items.
*
* @note The array to pass to this set function is different from the array
* that you provide to the constructor or set_from_array().
*
* @param array $arr
* * \e string \b allow_cid => string of allowed cids
* * \e string \b allow_gid => string of allowed gids
* * \e string \b deny_cid => string of denied cids
* * \e string \b deny_gid => string of denied gids
* @param bool $explicit (optional) default true
*/
public function set($arr, $explicit = true)
{
$this->allow_cid = $arr['allow_cid'];
$this->allow_gid = $arr['allow_gid'];
$this->deny_cid = $arr['deny_cid'];
$this->deny_gid = $arr['deny_gid'];
$this->explicit = $explicit;
}
/**
* @brief Return an array consisting of the current access list components
* where the elements are directly storable.
*
* @return array An associative array with:
* * \e string \b allow_cid => string of allowed cids
* * \e string \b allow_gid => string of allowed gids
* * \e string \b deny_cid => string of denied cids
* * \e string \b deny_gid => string of denied gids
*/
public function get()
{
return [
'allow_cid' => $this->allow_cid,
'allow_gid' => $this->allow_gid,
'deny_cid' => $this->deny_cid,
'deny_gid' => $this->deny_gid,
];
}
/**
* @brief Set access list components from arrays, such as those provided by
* acl_selector().
*
* For convenience, a string (or non-array) input is assumed to be a
* comma-separated list and auto-converted into an array.
*
* @note The array to pass to this set function is different from the array
* that you provide to the constructor or set().
*
* @param array $arr An associative array with:
* * \e array|string \b contact_allow => array with cids or comma-seperated string
* * \e array|string \b group_allow => array with gids or comma-seperated string
* * \e array|string \b contact_deny => array with cids or comma-seperated string
* * \e array|string \b group_deny => array with gids or comma-seperated string
* @param bool $explicit (optional) default true
*/
public function set_from_array($arr, $explicit = true)
{
$this->allow_cid = perms2str((is_array($arr['contact_allow']))
? $arr['contact_allow'] : explode(',', $arr['contact_allow']));
$this->allow_gid = perms2str((is_array($arr['group_allow']))
? $arr['group_allow'] : explode(',', $arr['group_allow']));
$this->deny_cid = perms2str((is_array($arr['contact_deny']))
? $arr['contact_deny'] : explode(',', $arr['contact_deny']));
$this->deny_gid = perms2str((is_array($arr['group_deny']))
? $arr['group_deny'] : explode(',', $arr['group_deny']));
$this->explicit = $explicit;
}
/**
* @brief Returns true if any access lists component is set.
*
* @return bool Return true if any of allow_* deny_* values is set.
*/
public function is_private()
{
return (($this->allow_cid || $this->allow_gid || $this->deny_cid || $this->deny_gid) ? true : false);
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace Code\Access;
use App;
use Code\Lib\PConfig;
use Code\Extend\Hook;
/**
* @brief Permission limits.
*
* Permission limits are a very high level permission setting. They are hard
* limits by design.
* "Who can view my photos (at all)?"
* "Who can post photos in my albums (at all)?"
*
* For viewing permissions we generally set these to 'anybody' and for write
* permissions we generally set them to 'those I allow', though many people
* restrict the viewing permissions further for things like 'Can view my connections'.
*
* People get confused enough by permissions that we wanted a place to set their
* privacy expectations once and be done with it.
*
* Connection related permissions like "Can Joe view my photos?" are handled by
* @ref ::Code::Lib::Permcat "Permcat" and inherit from the channel's Permission
* limits.
*
* @see Permissions
*/
class PermissionLimits
{
/**
* @brief Get standard permission limits.
*
* Viewing permissions and post_comments permission are set to 'anybody',
* other permissions are set to 'those I allow'.
*
* The list of permissions comes from Permissions::Perms().
*
* @return array
*/
public static function Std_Limits()
{
$limits = [];
$perms = Permissions::Perms();
foreach ($perms as $k => $v) {
if (strstr($k, 'view')) {
$limits[$k] = PERMS_PUBLIC;
} else {
$limits[$k] = PERMS_SPECIFIC;
}
}
return $limits;
}
/**
* @brief Sets a permission limit for a channel.
*
* @param int $channel_id
* @param string $perm
* @param int $perm_limit one of PERMS_* constants
*/
public static function Set($channel_id, $perm, $perm_limit)
{
PConfig::Set($channel_id, 'perm_limits', $perm, $perm_limit);
}
/**
* @brief Get a channel's permission limits.
*
* Return a channel's permission limits from PConfig. If $perm is set just
* return this permission limit, if not set, return an array with all
* permission limits.
*
* @param int $channel_id
* @param string $perm (optional)
* @return
* * \b false if no perm_limits set for this channel
* * \b int if $perm is set, return one of PERMS_* constants for this permission, default 0
* * \b array with all permission limits, if $perm is not set
*/
public static function Get($channel_id, $perm = '')
{
if (! intval($channel_id)) {
return false;
}
if ($perm) {
$x = PConfig::Get($channel_id, 'perm_limits', $perm);
if ($x === false) {
$a = [ 'channel_id' => $channel_id, 'permission' => $perm, 'value' => $x ];
Hook::call('permission_limits_get', $a);
return intval($a['value']);
}
return intval($x);
}
PConfig::Load($channel_id);
if (array_key_exists($channel_id, App::$config) && array_key_exists('perm_limits', App::$config[$channel_id])) {
return App::$config[$channel_id]['perm_limits'];
}
return false;
}
}

View file

@ -0,0 +1,155 @@
<?php
namespace Code\Access;
use Code\Extend\Hook;
/**
* @brief PermissionRoles class.
*
* @see Permissions
*/
class PermissionRoles
{
/**
* @brief PermissionRoles version.
*
* This must match the version in Permissions.php before permission updates can run.
*
* @return number
*/
public static function version()
{
return 3;
}
public static function role_perms($role)
{
$ret = [];
$ret['role'] = $role;
switch ($role) {
case 'social':
$ret['perms_auto'] = false;
$ret['default_collection'] = false;
$ret['directory_publish'] = true;
$ret['online'] = true;
$ret['perms_connect'] = [
'view_stream', 'view_profile', 'view_contacts', 'view_storage',
'view_pages', 'send_stream', 'post_mail', 'post_wall', 'post_comments'
];
$ret['limits'] = PermissionLimits::Std_Limits();
break;
case 'social_restricted':
$ret['perms_auto'] = false;
$ret['default_collection'] = true;
$ret['directory_publish'] = true;
$ret['online'] = false;
$ret['perms_connect'] = [
'view_stream', 'view_profile', 'view_storage',
'view_pages', 'send_stream', 'post_mail', 'post_wall', 'post_comments'
];
$ret['limits'] = PermissionLimits::Std_Limits();
$ret['limits']['view_contacts'] = PERMS_SPECIFIC;
break;
case 'forum':
$ret['perms_auto'] = true;
$ret['default_collection'] = false;
$ret['directory_publish'] = true;
$ret['online'] = false;
$ret['perms_connect'] = [
'view_stream', 'view_profile', 'view_contacts', 'view_storage', 'write_storage',
'view_pages', 'post_mail', 'post_wall', 'post_comments'
];
$ret['limits'] = PermissionLimits::Std_Limits();
$ret['channel_type'] = 'group';
break;
case 'forum_moderated':
$ret['perms_auto'] = true;
$ret['default_collection'] = false;
$ret['directory_publish'] = true;
$ret['online'] = false;
$ret['perms_connect'] = [
'view_stream', 'view_profile', 'view_contacts', 'view_storage',
'view_pages', 'post_mail', 'post_wall', 'post_comments', 'moderated'
];
$ret['limits'] = PermissionLimits::Std_Limits();
$ret['channel_type'] = 'group';
break;
case 'forum_restricted':
$ret['perms_auto'] = false;
$ret['default_collection'] = true;
$ret['directory_publish'] = true;
$ret['online'] = false;
$ret['perms_connect'] = [
'view_stream', 'view_profile', 'view_contacts', 'view_storage', 'write_storage',
'view_pages', 'post_mail', 'post_wall', 'post_comments'
];
$ret['limits'] = PermissionLimits::Std_Limits();
$ret['limits']['view_contacts'] = PERMS_SPECIFIC;
$ret['channel_type'] = 'group';
break;
default:
break;
}
$x = get_config('system', 'role_perms');
// let system settings over-ride any or all
if ($x && is_array($x) && array_key_exists($role, $x)) {
$ret = array_merge($ret, $x[$role]);
}
/**
* @hooks get_role_perms
* * \e array
*/
$x = ['role' => $role, 'result' => $ret];
Hook::call('get_role_perms', $x);
return $x['result'];
}
/**
* @brief Array with translated role names and grouping.
*
* Return an associative array with grouped role names that can be used
* to create select groups like in \e field_select_grouped.tpl.
*
* @return array
*/
public static function roles()
{
$roles = [
t('Social Networking') => [
'social' => t('Social - Normal'),
'social_restricted' => t('Social - Restricted')
],
t('Community Group') => [
'forum' => t('Group - Normal'),
'forum_restricted' => t('Group - Restricted'),
'forum_moderated' => t('Group - Moderated')
],
];
Hook::call('list_permission_roles', $roles);
return $roles;
}
}

298
Code/Access/Permissions.php Normal file
View file

@ -0,0 +1,298 @@
<?php
namespace Code\Access;
use Code\Lib as Zlib;
use Code\Lib\Channel;
use Code\Extend\Hook;
/**
* @brief Extensible permissions.
*
* To add new permissions, add to the list of $perms below, with a simple description.
*
* Also visit PermissionRoles.php and add to the $ret['perms_connect'] property for any role
* if this permission should be granted to new connections.
*
* Next look at PermissionRoles::new_custom_perms() and provide a handler for updating custom
* permission roles. You will want to set a default PermissionLimit for each channel and also
* provide a sane default for any existing connections. You may or may not wish to provide a
* default auto permission. If in doubt, leave this alone as custom permissions by definition
* are the responsibility of the channel owner to manage. You just don't want to create any
* suprises or break things so you have an opportunity to provide sane settings.
*
* Update the version here and in PermissionRoles.
*
*
* Permissions with 'view' in the name are considered read permissions. Anything
* else requires authentication. Read permission limits are PERMS_PUBLIC and anything else
* is given PERMS_SPECIFIC.
*
* PermissionLimits::Std_limits() retrieves the standard limits. A permission role
* MAY alter an individual setting after retrieving the Std_limits if you require
* something different for a specific permission within the given role.
*
*/
class Permissions
{
/**
* @brief Permissions version.
*
* This must match the version in PermissionRoles.php before permission updates can run.
*
* @return number
*/
public static function version()
{
return 3;
}
/**
* @brief Return an array with Permissions.
*
* @param string $filter (optional) only passed to hook permissions_list
* @return array Associative array with permissions and short description.
*/
public static function Perms($filter = '')
{
$perms = [
'view_stream' => t('Grant viewing access to and delivery of your channel stream and posts'),
'view_profile' => t('Grant viewing access to your default channel profile'),
'view_contacts' => t('Grant viewing access to your address book (connections)'),
'view_storage' => t('Grant viewing access to your file storage and photos'),
'post_wall' => t('Grant permission to post on your channel (wall) page'),
'post_mail' => t('Accept delivery of direct messages and personal mail'),
'send_stream' => t('Accept delivery of their posts and all comments to their posts'),
'post_comments' => t('Accept delivery of their comments and likes on your posts'),
'write_storage' => t('Grant upload permissions to your file storage and photos'),
'republish' => t('Grant permission to republish/mirror your posts'),
'moderated' => t('Accept comments and wall posts only after approval (moderation)'),
'delegate' => t('Grant channel administration (delegation) permission')
];
$x = [
'permissions' => $perms,
'filter' => $filter
];
/**
* @hooks permissions_list
* * \e array \b permissions
* * \e string \b filter
*/
Hook::call('permissions_list', $x);
return($x['permissions']);
}
/**
* @brief Perms from the above list that are blocked from anonymous observers.
*
* e.g. you must be authenticated.
*
* @return array Associative array with permissions and short description.
*/
public static function BlockedAnonPerms()
{
$res = [];
$perms = PermissionLimits::Std_limits();
foreach ($perms as $perm => $limit) {
if ($limit != PERMS_PUBLIC) {
$res[] = $perm;
}
}
$x = ['permissions' => $res];
/**
* @hooks write_perms
* * \e array \b permissions
*/
Hook::call('write_perms', $x);
return($x['permissions']);
}
/**
* @brief Converts indexed perms array to associative perms array.
*
* Converts [ 0 => 'view_stream', ... ]
* to [ 'view_stream' => 1 ] for any permissions in $arr;
* Undeclared permissions which exist in Perms() are added and set to 0.
*
* @param array $arr
* @return array
*/
public static function FilledPerms($arr)
{
if (is_null($arr) || (! is_array($arr))) {
btlogger('FilledPerms: ' . print_r($arr, true));
$arr = [];
}
$everything = self::Perms();
$ret = [];
foreach ($everything as $k => $v) {
if (in_array($k, $arr)) {
$ret[$k] = 1;
} else {
$ret[$k] = 0;
}
}
return $ret;
}
/**
* @brief Convert perms array to indexed array.
*
* Converts [ 'view_stream' => 1 ] for any permissions in $arr
* to [ 0 => ['name' => 'view_stream', 'value' => 1], ... ]
*
* @param array $arr associative perms array 'view_stream' => 1
* @return array Indexed array with elements that look like
* * \e string \b name the perm name (e.g. view_stream)
* * \e int \b value the value of the perm (e.g. 1)
*/
public static function OPerms($arr)
{
$ret = [];
if ($arr) {
foreach ($arr as $k => $v) {
$ret[] = [ 'name' => $k, 'value' => $v ];
}
}
return $ret;
}
/**
* @brief
*
* @param int $channel_id
* @return bool|array
*/
public static function FilledAutoperms($channel_id)
{
if (! intval(get_pconfig($channel_id, 'system', 'autoperms'))) {
return false;
}
$arr = [];
$r = q(
"select * from pconfig where uid = %d and cat = 'autoperms'",
intval($channel_id)
);
if ($r) {
foreach ($r as $rr) {
$arr[$rr['k']] = intval($rr['v']);
}
}
return $arr;
}
/**
* @brief Compares that all Permissions from $p1 exist also in $p2.
*
* @param array $p1 The perms that have to exist in $p2
* @param array $p2 The perms to compare against
* @return bool true if all perms from $p1 exist also in $p2
*/
public static function PermsCompare($p1, $p2)
{
foreach ($p1 as $k => $v) {
if (! array_key_exists($k, $p2)) {
return false;
}
if ($p1[$k] != $p2[$k]) {
return false;
}
}
return true;
}
/**
* @brief
*
* @param int $channel_id A channel id
* @return array Associative array with
* * \e array \b perms Permission array
* * \e int \b automatic 0 or 1
*/
public static function connect_perms($channel_id)
{
$my_perms = [];
$permcat = null;
$automatic = 0;
// If a default permcat exists, use that
$pc = ((Zlib\Apps::system_app_installed($channel_id, 'Roles')) ? get_pconfig($channel_id, 'system', 'default_permcat') : 'default');
if (! in_array($pc, [ '','default' ])) {
$pcp = new Zlib\Permcat($channel_id);
$permcat = $pcp->fetch($pc);
if ($permcat && $permcat['perms']) {
foreach ($permcat['perms'] as $p) {
$my_perms[$p['name']] = $p['value'];
}
}
}
$automatic = intval(get_pconfig($channel_id, 'system', 'autoperms'));
// look up the permission role to see if it specified auto-connect
// and if there was no permcat or a default permcat, set the perms
// from the role
$role = get_pconfig($channel_id, 'system', 'permissions_role');
if ($role) {
$xx = PermissionRoles::role_perms($role);
if ((! $my_perms) && ($xx['perms_connect'])) {
$default_perms = $xx['perms_connect'];
$my_perms = Permissions::FilledPerms($default_perms);
}
}
// If we reached this point without having any permission information,
// it is likely a custom permissions role. First see if there are any
// automatic permissions.
if (! $my_perms) {
$m = Permissions::FilledAutoperms($channel_id);
if ($m) {
$my_perms = $m;
}
}
// If we reached this point with no permissions, the channel is using
// custom perms but they are not automatic. They will be stored in abconfig with
// the channel's channel_hash (the 'self' connection).
if (! $my_perms) {
$c = Channel::from_id($channel_id);
if ($c) {
$my_perms = Permissions::FilledPerms(explode(',', get_abconfig($channel_id, $c['channel_hash'], 'system', 'my_perms', EMPTY_STR)));
}
}
return ( [ 'perms' => $my_perms, 'automatic' => $automatic ] );
}
public static function serialise($p)
{
$n = [];
if ($p) {
foreach ($p as $k => $v) {
if (intval($v)) {
$n[] = $k;
}
}
}
return implode(',', $n);
}
}

15
Code/Daemon/Addon.php Normal file
View file

@ -0,0 +1,15 @@
<?php
namespace Code\Daemon;
use Code\Extend\Hook;
class Addon
{
public static function run($argc, $argv)
{
Hook::call('daemon_addon', $argv);
}
}

View file

@ -0,0 +1,54 @@
<?php
/** @file */
namespace Code\Daemon;
require_once('include/photos.php');
class CacheThumb
{
public static function run($argc, $argv)
{
if (! $argc == 2) {
return;
}
$path = 'cache/img/' . substr($argv[1], 0, 2) . '/' . $argv[1];
$is = getimagesize($path);
if (! $is) {
return;
}
$width = $is[0];
$height = $is[1];
$max_thumb = get_config('system', 'max_cache_thumbnail', 1024);
if ($width > $max_thumb || $height > $max_thumb) {
$imagick_path = get_config('system', 'imagick_convert_path');
if ($imagick_path && @file_exists($imagick_path)) {
$tmp_name = $path . '-001';
$newsize = photo_calculate_scale(array_merge($is, ['max' => $max_thumb]));
$cmd = $imagick_path . ' ' . escapeshellarg(PROJECT_BASE . '/' . $path) . ' -resize ' . $newsize . ' ' . escapeshellarg(PROJECT_BASE . '/' . $tmp_name);
for ($x = 0; $x < 4; $x++) {
exec($cmd);
if (file_exists($tmp_name)) {
break;
}
continue;
}
if (! file_exists($tmp_name)) {
return;
}
@rename($tmp_name, $path);
}
}
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Code\Daemon;
class Cache_embeds
{
public static function run($argc, $argv)
{
if (! $argc == 2) {
return;
}
$c = q(
"select body, html, created from item where id = %d ",
dbesc(intval($argv[1]))
);
if (! $c) {
return;
}
$item = array_shift($c);
$cache_expire = intval(get_config('system', 'default_expire_days'));
if ($cache_expire <= 0) {
$cache_expire = 60;
}
$cache_enable = ((($cache_expire) && ($item['created'] < datetime_convert('UTC', 'UTC', 'now - ' . $cache_expire . ' days'))) ? false : true);
$s = bbcode($item['body']);
$s = sslify($s, $cache_enable);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Code\Daemon;
use Code\Lib\Img_cache;
class Cache_image
{
public static function run($argc, $argv)
{
cli_startup();
logger('caching: ' . $argv[1] . ' to ' . $argv[2]);
if ($argc === 3) {
Img_cache::url_to_cache($argv[1], $argv[2]);
}
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Code\Daemon;
use Code\Lib\Channel;
class Channel_purge
{
public static function run($argc, $argv)
{
cli_startup();
$channel_id = intval($argv[1]);
$channel = q(
"select * from channel where channel_id = %d and channel_removed = 1",
intval($channel_id)
);
if (! $channel) {
return;
}
do {
$r = q(
"select id from item where uid = %d and item_deleted = 0 limit 1000",
intval($channel_id)
);
if ($r) {
foreach ($r as $rv) {
drop_item($rv['id'], false);
}
}
} while ($r);
}
}

View file

@ -0,0 +1,66 @@
<?php
/** @file */
namespace Code\Daemon;
require_once('include/hubloc.php');
class Checksites
{
public static function run($argc, $argv)
{
logger('checksites: start');
if (($argc > 1) && ($argv[1])) {
$site_id = $argv[1];
}
if ($site_id) {
$sql_options = " and site_url = '" . dbesc($argv[1]) . "' ";
}
$days = intval(get_config('system', 'sitecheckdays'));
if ($days < 1) {
$days = 30;
}
$r = q(
"select * from site where site_dead = 0 and site_update < %s - INTERVAL %s and site_type = %d $sql_options ",
db_utcnow(),
db_quoteinterval($days . ' DAY'),
intval(SITE_TYPE_ZOT)
);
if (! $r) {
return;
}
foreach ($r as $rr) {
if (! strcasecmp($rr['site_url'], z_root())) {
continue;
}
$x = ping_site($rr['site_url']);
if ($x['success']) {
logger('checksites: ' . $rr['site_url']);
q(
"update site set site_update = '%s' where site_url = '%s' ",
dbesc(datetime_convert()),
dbesc($rr['site_url'])
);
} else {
logger('marking dead site: ' . $x['message']);
q(
"update site set site_dead = 1 where site_url = '%s' ",
dbesc($rr['site_url'])
);
}
}
return;
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace Code\Daemon;
use Code\Web\HTTPSig;
use Code\Lib\Channel;
require_once('include/cli_startup.php');
require_once('include/attach.php');
require_once('include/import.php');
class Content_importer
{
public static function run($argc, $argv)
{
cli_startup();
$page = $argv[1];
$since = $argv[2];
$until = $argv[3];
$channel_address = $argv[4];
$hz_server = urldecode($argv[5]);
$m = parse_url($hz_server);
$channel = Channel::from_username($channel_address);
if (! $channel) {
logger('itemhelper: channel not found');
killme();
}
$headers = [
'X-API-Token' => random_string(),
'X-API-Request' => $hz_server . '/api/z/1.0/item/export_page?f=&zap_compat=1&since=' . urlencode($since) . '&until=' . urlencode($until) . '&page=' . $page ,
'Host' => $m['host'],
'(request-target)' => 'get /api/z/1.0/item/export_page?f=&zap_compat=1&since=' . urlencode($since) . '&until=' . urlencode($until) . '&page=' . $page ,
];
$headers = HTTPSig::create_sig($headers, $channel['channel_prvkey'], Channel::url($channel), true, 'sha512');
$x = z_fetch_url($hz_server . '/api/z/1.0/item/export_page?f=&zap_compat=1&since=' . urlencode($since) . '&until=' . urlencode($until) . '&page=' . $page, false, $redirects, [ 'headers' => $headers ]);
if (! $x['success']) {
logger('no API response', LOGGER_DEBUG);
killme();
}
$j = json_decode($x['body'], true);
if (! $j) {
killme();
}
if (! ($j['item'] || count($j['item']))) {
killme();
}
import_items($channel, $j['item'], false, ((array_key_exists('relocate', $j)) ? $j['relocate'] : null));
killme();
}
}

62
Code/Daemon/Convo.php Normal file
View file

@ -0,0 +1,62 @@
<?php
namespace Code\Daemon;
use Code\Lib\Activity;
use Code\Lib\ActivityStreams;
use Code\Lib\ASCollection;
use Code\Lib\Channel;
class Convo
{
public static function run($argc, $argv)
{
logger('convo invoked: ' . print_r($argv, true));
if ($argc != 4) {
killme();
}
$id = $argv[1];
$channel_id = intval($argv[2]);
$contact_hash = $argv[3];
$channel = Channel::from_id($channel_id);
if (! $channel) {
killme();
}
$r = q(
"SELECT abook.*, xchan.* FROM abook left join xchan on abook_xchan = xchan_hash
WHERE abook_channel = %d and abook_xchan = '%s' LIMIT 1",
intval($channel_id),
dbesc($contact_hash)
);
if (! $r) {
killme();
}
$contact = array_shift($r);
$obj = new ASCollection($id, $channel);
$messages = $obj->get();
if ($messages) {
foreach ($messages as $message) {
if (is_string($message)) {
$message = Activity::fetch($message, $channel);
}
// set client flag because comments will probably just be objects and not full blown activities
// and that lets us use implied_create
$AS = new ActivityStreams($message, null, true);
if ($AS->is_valid() && is_array($AS->obj)) {
$item = Activity::decode_note($AS, true);
Activity::store($channel, $contact['abook_xchan'], $AS, $item, true, true);
}
}
}
}
}

223
Code/Daemon/Cron.php Normal file
View file

@ -0,0 +1,223 @@
<?php
/** @file */
namespace Code\Daemon;
use Code\Lib\Libsync;
use Code\Lib\Channel;
use Code\Lib\Addon;
class Cron
{
public static function run($argc, $argv)
{
$maxsysload = intval(get_config('system', 'maxloadavg'));
if ($maxsysload < 1) {
$maxsysload = 50;
}
if (function_exists('sys_getloadavg')) {
$load = sys_getloadavg();
if (intval($load[0]) > $maxsysload) {
logger('system: load ' . $load . ' too high. Cron deferred to next scheduled run.');
return;
}
}
// Check for a lockfile. If it exists, but is over an hour old, it's stale. Ignore it.
$lockfile = 'cache/cron';
if (
(file_exists($lockfile)) && (filemtime($lockfile) > (time() - 3600))
&& (! get_config('system', 'override_cron_lockfile'))
) {
logger("cron: Already running");
return;
}
// Create a lockfile. Needs two vars, but $x doesn't need to contain anything.
file_put_contents($lockfile, $x);
logger('cron: start');
// run queue delivery process in the background
Run::Summon([ 'Queue' ]);
Run::Summon([ 'Poller' ]);
// maintenance for mod sharedwithme - check for updated items and remove them
require_once('include/sharedwithme.php');
apply_updates();
// expire any expired items
$r = q(
"select id,item_wall from item where expires > '2001-01-01 00:00:00' and expires < %s
and item_deleted = 0 ",
db_utcnow()
);
if ($r) {
require_once('include/items.php');
foreach ($r as $rr) {
drop_item($rr['id'], false, (($rr['item_wall']) ? DROPITEM_PHASE1 : DROPITEM_NORMAL));
if ($rr['item_wall']) {
// The notifier isn't normally invoked unless item_drop is interactive.
Run::Summon([ 'Notifier', 'drop', $rr['id'] ]);
}
}
}
// delete expired access tokens
$r = q(
"select atoken_id from atoken where atoken_expires > '%s' and atoken_expires < %s",
dbesc(NULL_DATE),
db_utcnow()
);
if ($r) {
require_once('include/security.php');
foreach ($r as $rr) {
atoken_delete($rr['atoken_id']);
}
}
// Ensure that every channel pings their directory occasionally.
$r = q(
"select channel_id from channel where channel_dirdate < %s - INTERVAL %s and channel_removed = 0",
db_utcnow(),
db_quoteinterval('7 DAY')
);
if ($r) {
foreach ($r as $rr) {
Run::Summon([ 'Directory', $rr['channel_id'], 'force' ]);
if ($interval) {
@time_sleep_until(microtime(true) + (float) $interval);
}
}
}
// publish any applicable items that were set to be published in the future
// (time travel posts). Restrict to items that have come of age in the last
// couple of days to limit the query to something reasonable.
$r = q(
"select id from item where item_delayed = 1 and created <= %s and created > '%s' ",
db_utcnow(),
dbesc(datetime_convert('UTC', 'UTC', 'now - 2 days'))
);
if ($r) {
foreach ($r as $rr) {
$x = q(
"update item set item_delayed = 0 where id = %d",
intval($rr['id'])
);
if ($x) {
$z = q(
"select * from item where id = %d",
intval($message_id)
);
if ($z) {
xchan_query($z);
$sync_item = fetch_post_tags($z);
Libsync::build_sync_packet(
$sync_item[0]['uid'],
[
'item' => [ encode_item($sync_item[0], true) ]
]
);
}
Run::Summon([ 'Notifier','wall-new',$rr['id'] ]);
}
}
}
require_once('include/attach.php');
attach_upgrade();
$abandon_days = intval(get_config('system', 'account_abandon_days'));
if ($abandon_days < 1) {
$abandon_days = 0;
}
// once daily run birthday_updates and then expire in background
// FIXME: add birthday updates, both locally and for xprof for use
// by directory servers
$d1 = intval(get_config('system', 'last_expire_day'));
$d2 = intval(datetime_convert('UTC', 'UTC', 'now', 'd'));
// Allow somebody to staggger daily activities if they have more than one site on their server,
// or if it happens at an inconvenient (busy) hour.
$h1 = intval(get_config('system', 'cron_hour'));
$h2 = intval(datetime_convert('UTC', 'UTC', 'now', 'G'));
if (($d2 != $d1) && ($h1 == $h2)) {
Run::Summon([ 'Cron_daily' ]);
}
// update any photos which didn't get imported properly
// This should be rare
$r = q(
"select xchan_photo_l, xchan_hash from xchan where xchan_photo_l != '' and xchan_photo_m = ''
and xchan_photo_date < %s - INTERVAL %s",
db_utcnow(),
db_quoteinterval('1 DAY')
);
if ($r) {
require_once('include/photo_factory.php');
foreach ($r as $rr) {
$photos = import_remote_xchan_photo($rr['xchan_photo_l'], $rr['xchan_hash']);
if ($photos) {
$x = q(
"update xchan set xchan_photo_l = '%s', xchan_photo_m = '%s', xchan_photo_s = '%s', xchan_photo_mimetype = '%s'
where xchan_hash = '%s'",
dbesc($photos[0]),
dbesc($photos[1]),
dbesc($photos[2]),
dbesc($photos[3]),
dbesc($rr['xchan_hash'])
);
}
}
}
$generation = 0;
$restart = false;
if (($argc > 1) && ($argv[1] == 'restart')) {
$restart = true;
$generation = intval($argv[2]);
if (! $generation) {
return;
}
}
Addon::reload_all();
$d = datetime_convert();
// TODO check to see if there are any cronhooks before wasting a process
if (! $restart) {
Run::Summon([ 'Cronhooks' ]);
}
set_config('system', 'lastcron', datetime_convert());
//All done - clear the lockfile
@unlink($lockfile);
return;
}
}

117
Code/Daemon/Cron_daily.php Normal file
View file

@ -0,0 +1,117 @@
<?php
namespace Code\Daemon;
use Code\Lib\ServiceClass;
use Code\Lib\Libzotdir;
use Code\Lib\Libzot;
use Code\Lib\Statistics;
use Code\Extend\Hook;
class Cron_daily
{
public static function run($argc, $argv)
{
logger('cron_daily: start');
/**
* Cron Daily
*
*/
// make sure our own site record is up to date
Libzot::import_site(Libzot::site_info(true));
// Fire off the Cron_weekly process if it's the correct day.
$d3 = intval(datetime_convert('UTC', 'UTC', 'now', 'N'));
if ($d3 == 7) {
Run::Summon([ 'Cron_weekly' ]);
}
// once daily run birthday_updates and then expire in background
// FIXME: add birthday updates, both locally and for xprof for use
// by directory servers
update_birthdays();
// expire any read notifications over a month old
q(
"delete from notify where seen = 1 and created < %s - INTERVAL %s",
db_utcnow(),
db_quoteinterval('60 DAY')
);
// expire any unread notifications over a year old
q(
"delete from notify where seen = 0 and created < %s - INTERVAL %s",
db_utcnow(),
db_quoteinterval('1 YEAR')
);
// update statistics counters (ignore values, they will be stored in config)
Statistics::get_channels_all();
Statistics::get_channels_6mo();
Statistics::get_channels_1mo();
Statistics::get_posts();
Statistics::get_comments();
// expire old delivery reports
$keep_reports = intval(get_config('system', 'expire_delivery_reports'));
if ($keep_reports === 0) {
$keep_reports = 10;
}
q(
"delete from dreport where dreport_time < %s - INTERVAL %s",
db_utcnow(),
db_quoteinterval($keep_reports . ' DAY')
);
// delete accounts that did not submit email verification within 3 days
$r = q(
"select * from register where password = 'verify' and created < %s - INTERVAL %s",
db_utcnow(),
db_quoteinterval('3 DAY')
);
if ($r) {
foreach ($r as $rv) {
q(
"DELETE FROM account WHERE account_id = %d",
intval($rv['uid'])
);
q(
"DELETE FROM register WHERE id = %d",
intval($rv['id'])
);
}
}
// expire any expired accounts
ServiceClass::downgrade_accounts();
Run::Summon([ 'Expire' ]);
remove_obsolete_hublocs();
Hook::call('cron_daily', datetime_convert());
set_config('system', 'last_expire_day', intval(datetime_convert('UTC', 'UTC', 'now', 'd')));
/**
* End Cron Daily
*/
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace Code\Daemon;
use Code\Lib\Channel;
use Code\Extend\Hook;
class Cron_weekly
{
public static function run($argc, $argv)
{
/**
* Cron Weekly
*
* Actions in the following block are executed once per day only on Sunday (once per week).
*
*/
Hook::call('cron_weekly', datetime_convert());
z_check_cert();
prune_hub_reinstalls();
mark_orphan_hubsxchans();
// Find channels that were removed in the last three weeks, but
// haven't been finally cleaned up. These should be older than 10
// days to ensure that "purgeall" messages have gone out or bounced
// or timed out.
$r = q(
"select channel_id from channel where channel_removed = 1 and
channel_deleted > %s - INTERVAL %s and channel_deleted < %s - INTERVAL %s",
db_utcnow(),
db_quoteinterval('21 DAY'),
db_utcnow(),
db_quoteinterval('10 DAY')
);
if ($r) {
foreach ($r as $rv) {
Channel::channel_remove_final($rv['channel_id']);
}
}
// get rid of really old poco records
q(
"delete from xlink where xlink_updated < %s - INTERVAL %s and xlink_static = 0 ",
db_utcnow(),
db_quoteinterval('14 DAY')
);
// Check for dead sites
Run::Summon(['Checksites' ]);
// clean up image cache - use site expiration or 60 days if not set or zero
$files = glob('cache/img/*/*');
$expire_days = intval(get_config('system', 'default_expire_days'));
if ($expire_days <= 0) {
$expire_days = 60;
}
$now = time();
$maxage = 86400 * $expire_days;
if ($files) {
foreach ($files as $file) {
if (is_file($file)) {
if ($now - filemtime($file) >= $maxage) {
unlink($file);
}
}
}
}
// update searchable doc indexes
Run::Summon([ 'Importdoc']);
/**
* End Cron Weekly
*/
}
}

22
Code/Daemon/Cronhooks.php Normal file
View file

@ -0,0 +1,22 @@
<?php
/** @file */
namespace Code\Daemon;
use Code\Extend\Hook;
class Cronhooks
{
public static function run($argc, $argv)
{
logger('cronhooks: start');
$d = datetime_convert();
Hook::call('cron', $d);
return;
}
}

60
Code/Daemon/CurlAuth.php Normal file
View file

@ -0,0 +1,60 @@
<?php
namespace Code\Daemon;
// generate a curl compatible cookie file with an authenticated session for the given channel_id.
// If this file is then used with curl and the destination url is sent through zid() or manually
// manipulated to add a zid, it should allow curl to provide zot magic-auth across domains.
// Handles expiration of stale cookies currently by deleting them and rewriting the file.
use App;
class CurlAuth
{
public static function run($argc, $argv)
{
if ($argc != 2) {
return;
}
App::$session->start();
$_SESSION['authenticated'] = 1;
$_SESSION['uid'] = $argv[1];
$x = session_id();
$f = 'cache/cookie_' . $argv[1];
$c = 'cache/cookien_' . $argv[1];
$e = file_exists($f);
$output = '';
if ($e) {
$lines = file($f);
if ($lines) {
foreach ($lines as $line) {
if (strlen($line) > 0 && $line[0] != '#' && substr_count($line, "\t") == 6) {
$tokens = explode("\t", $line);
$tokens = array_map('trim', $tokens);
if ($tokens[4] > time()) {
$output .= $line . "\n";
}
} else {
$output .= $line;
}
}
}
}
$t = time() + (24 * 3600);
file_put_contents($f, $output . 'HttpOnly_' . App::get_hostname() . "\tFALSE\t/\tTRUE\t$t\tPHPSESSID\t" . $x, (($e) ? FILE_APPEND : 0));
file_put_contents($c, $x);
return;
}
}

36
Code/Daemon/Deliver.php Normal file
View file

@ -0,0 +1,36 @@
<?php
/** @file */
namespace Code\Daemon;
use Code\Lib\Libzot;
use Code\Lib\Queue;
class Deliver
{
public static function run($argc, $argv)
{
if ($argc < 2) {
return;
}
logger('deliver: invoked: ' . print_r($argv, true), LOGGER_DATA);
for ($x = 1; $x < $argc; $x++) {
if (! $argv[$x]) {
continue;
}
$r = q(
"select * from outq where outq_hash = '%s' limit 1",
dbesc($argv[$x])
);
if ($r) {
Queue::deliver($r[0], true);
}
}
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Code\Daemon;
use Code\Extend\Hook;
class Deliver_hooks
{
public static function run($argc, $argv)
{
if ($argc < 2) {
return;
}
$r = q(
"select * from item where id = '%d'",
intval($argv[1])
);
if ($r) {
Hook::call('notifier_normal', $r[0]);
}
}
}

25
Code/Daemon/Delxitems.php Normal file
View file

@ -0,0 +1,25 @@
<?php
namespace Code\Daemon;
/*
* Daemon to remove 'item' resources in the background from a removed connection
*/
class Delxitems
{
public static function run($argc, $argv)
{
cli_startup();
if ($argc != 3) {
return;
}
remove_abook_items($argv[1], $argv[2]);
return;
}
}

54
Code/Daemon/Directory.php Normal file
View file

@ -0,0 +1,54 @@
<?php
namespace Code\Daemon;
use Code\Lib\Libzot;
use Code\Lib\Libzotdir;
use Code\Lib\Queue;
use Code\Lib\Channel;
class Directory
{
public static function run($argc, $argv)
{
if ($argc < 2) {
return;
}
$force = false;
$pushall = true;
if ($argc > 2) {
if ($argv[2] === 'force') {
$force = true;
}
if ($argv[2] === 'nopush') {
$pushall = false;
}
}
logger('directory update', LOGGER_DEBUG);
$channel = Channel::from_id($argv[1]);
if (! $channel) {
return;
}
// update the local directory - was optional, but now done regardless
Libzotdir::local_dir_update($argv[1], $force);
q(
"update channel set channel_dirdate = '%s' where channel_id = %d",
dbesc(datetime_convert()),
intval($channel['channel_id'])
);
// Now update all the connections
if ($pushall) {
Run::Summon([ 'Notifier','refresh_all',$channel['channel_id'] ]);
}
}
}

98
Code/Daemon/Expire.php Normal file
View file

@ -0,0 +1,98 @@
<?php
namespace Code\Daemon;
use Code\Lib\ServiceClass;
use Code\Lib\Channel;
class Expire
{
public static function run($argc, $argv)
{
cli_startup();
// perform final cleanup on previously delete items
$r = q(
"select id from item where item_deleted = 1 and item_pending_remove = 0 and changed < %s - INTERVAL %s",
db_utcnow(),
db_quoteinterval('10 DAY')
);
if ($r) {
foreach ($r as $rr) {
drop_item($rr['id'], false, DROPITEM_PHASE2);
}
}
// physically remove anything that has been deleted for more than two months
/** @FIXME - this is a wretchedly inefficient query */
$r = q(
"delete from item where item_pending_remove = 1 and changed < %s - INTERVAL %s",
db_utcnow(),
db_quoteinterval('36 DAY')
);
logger('expire: start', LOGGER_DEBUG);
$site_expire = intval(get_config('system', 'default_expire_days'));
$commented_days = intval(get_config('system', 'active_expire_days'));
logger('site_expire: ' . $site_expire);
$r = q("SELECT channel_id, channel_system, channel_address, channel_expire_days from channel where true");
if ($r) {
foreach ($r as $rr) {
// expire the sys channel separately
if (intval($rr['channel_system'])) {
continue;
}
// service class default (if non-zero) over-rides the site default
$service_class_expire = ServiceClass::fetch($rr['channel_id'], 'expire_days');
if (intval($service_class_expire)) {
$channel_expire = $service_class_expire;
} else {
$channel_expire = $site_expire;
}
if (
intval($channel_expire) && (intval($channel_expire) < intval($rr['channel_expire_days'])) ||
intval($rr['channel_expire_days'] == 0)
) {
$expire_days = $channel_expire;
} else {
$expire_days = $rr['channel_expire_days'];
}
// if the site or service class expiration is non-zero and less than person expiration, use that
logger('Expire: ' . $rr['channel_address'] . ' interval: ' . $expire_days, LOGGER_DEBUG);
item_expire($rr['channel_id'], $expire_days, $commented_days);
}
}
$x = Channel::get_system();
if ($x) {
// this should probably just fetch the channel_expire_days from the sys channel,
// but there's no convenient way to set it.
$expire_days = get_config('system', 'sys_expire_days', 30);
if (intval($site_expire) && (intval($site_expire) < intval($expire_days))) {
$expire_days = $site_expire;
}
logger('Expire: sys interval: ' . $expire_days, LOGGER_DEBUG);
if ($expire_days) {
item_expire($x['channel_id'], $expire_days, $commented_days);
}
logger('Expire: sys: done', LOGGER_DEBUG);
}
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Code\Daemon;
use Code\Web\HTTPSig;
use Code\Lib\Channel;
require_once('include/cli_startup.php');
require_once('include/attach.php');
require_once('include/import.php');
class File_importer
{
public static function run($argc, $argv)
{
cli_startup();
$attach_id = $argv[1];
$channel_address = $argv[2];
$hz_server = urldecode($argv[3]);
$m = parse_url($hz_server);
$channel = Channel::from_username($channel_address);
if (! $channel) {
logger('filehelper: channel not found');
killme();
}
$headers = [
'X-API-Token' => random_string(),
'X-API-Request' => $hz_server . '/api/z/1.0/file/export?f=&zap_compat=1&file_id=' . $attach_id,
'Host' => $m['host'],
'(request-target)' => 'get /api/z/1.0/file/export?f=&zap_compat=1&file_id=' . $attach_id,
];
$headers = HTTPSig::create_sig($headers, $channel['channel_prvkey'], Channel::url($channel), true, 'sha512');
$x = z_fetch_url($hz_server . '/api/z/1.0/file/export?f=&zap_compat=1&file_id=' . $attach_id, false, $redirects, [ 'headers' => $headers ]);
if (! $x['success']) {
logger('no API response', LOGGER_DEBUG);
return;
}
$j = json_decode($x['body'], true);
$r = sync_files($channel, [$j]);
killme();
}
}

66
Code/Daemon/Gprobe.php Normal file
View file

@ -0,0 +1,66 @@
<?php
/** @file */
namespace Code\Daemon;
use Code\Lib\Libzot;
use Code\Lib\Webfinger;
use Code\Lib\Zotfinger;
// performs zot_finger on $argv[1], which is a hex_encoded webbie/reddress
class Gprobe
{
public static function run($argc, $argv)
{
if ($argc != 2) {
return;
}
$url = hex2bin($argv[1]);
$protocols = [];
if (! strpos($url, '@')) {
return;
}
$r = q(
"select * from hubloc where hubloc_addr = '%s'",
dbesc($url)
);
if ($r) {
foreach ($r as $rv) {
if ($rv['hubloc_network'] === 'activitypub') {
$protocols[] = 'activitypub';
continue;
}
if ($rv['hubloc_network'] === 'nomad') {
$protocols[] = 'nomad';
$protocols[] = 'zot6';
continue;
}
if ($rv['hubloc_network'] === 'zot6') {
$protocols[] = 'zot6';
continue;
}
}
}
if (! in_array('zot6', $protocols)) {
$href = Webfinger::zot_url(punify($url));
if ($href) {
$zf = Zotfinger::exec($href, $channel);
}
if (is_array($zf) && array_path_exists('signature/signer', $zf) && $zf['signature']['signer'] === $href && intval($zf['signature']['header_valid']) && isset($zf['data']) && $zf['data']) {
$xc = Libzot::import_xchan($zf['data']);
}
}
return;
}
}

51
Code/Daemon/Importdoc.php Executable file
View file

@ -0,0 +1,51 @@
<?php
namespace Code\Daemon;
class Importdoc
{
public static function run($argc, $argv)
{
require_once('include/help.php');
self::update_docs_dir('doc/*');
}
public static function update_docs_dir($s)
{
$f = basename($s);
$d = dirname($s);
if ($s === 'doc/html') {
return;
}
$files = glob("$d/$f");
if ($files) {
foreach ($files as $fi) {
if ($fi === 'doc/html') {
continue;
}
if (is_dir($fi)) {
self::update_docs_dir("$fi/*");
} else {
// don't update media content
if (strpos(z_mime_content_type($fi), 'text') === 0) {
store_doc_file($fi);
}
}
}
}
// remove old files that weren't updated (indicates they were most likely deleted).
$i = q(
"select * from item where item_type = 5 and edited < %s - INTERVAL %s",
db_utcnow(),
db_quoteinterval('14 DAY')
);
if ($i) {
foreach ($i as $iv) {
drop_item($iv['id'], false, DROPITEM_NORMAL, true);
}
}
}
}

View file

@ -0,0 +1,58 @@
<?php
/** @file */
namespace Code\Daemon;
use Code\Lib\Libsync;
use Code\Lib\Channel;
class Importfile
{
public static function run($argc, $argv)
{
logger('Importfile: ' . print_r($argv, true));
if ($argc < 3) {
return;
}
$channel = Channel::from_id($argv[1]);
if (! $channel) {
return;
}
$srcfile = $argv[2];
$folder = (($argc > 3) ? $argv[3] : '');
$dstname = (($argc > 4) ? $argv[4] : '');
$hash = random_string();
$arr = [
'src' => $srcfile,
'filename' => (($dstname) ? $dstname : basename($srcfile)),
'hash' => $hash,
'allow_cid' => $channel['channel_allow_cid'],
'allow_gid' => $channel['channel_allow_gid'],
'deny_cid' => $channel['channel_deny_cid'],
'deny_gid' => $channel['channel_deny_gid'],
'preserve_original' => true,
'replace' => true
];
if ($folder) {
$arr['folder'] = $folder;
}
attach_store($channel, $channel['channel_hash'], 'import', $arr);
$sync = attach_export_data($channel, $hash);
if ($sync) {
Libsync::build_sync_packet($channel['channel_id'], array('file' => array($sync)));
}
return;
}
}

816
Code/Daemon/Notifier.php Normal file
View file

@ -0,0 +1,816 @@
<?php
namespace Code\Daemon;
use Code\Lib\Libzot;
use Code\Lib\Queue;
use Code\Lib\Activity;
use Code\Lib\ActivityStreams;
use Code\Lib\ActivityPub;
use Code\Lib\LDSignatures;
use Code\Lib\Channel;
use Code\Extend\Hook;
require_once('include/html2plain.php');
require_once('include/conversation.php');
require_once('include/bbcode.php');
/*
* Notifier - message dispatch and preparation for delivery
*
* The basic flow is:
* Identify the type of message
* Collect any information that needs to be sent
* Convert it into a suitable generic format for sending
* Figure out who the recipients are and if we need to relay
* through a conversation owner
* Once we know what recipients are involved, collect a list of
* destination sites
* Build and store a queue item for each unique site and invoke
* a delivery process for each site or a small number of sites (1-3)
* and add a slight delay between each delivery invocation if desired (usually)
*
*/
/*
* The notifier is typically called with:
*
* Code\Daemon\Run::Summon( [ 'Notifier', COMMAND, ITEM_ID ] );
*
* where COMMAND is one of the following:
*
* activity (in diaspora.php, dfrn_confirm.php, profiles.php)
* comment-import (in diaspora.php, items.php)
* comment-new (in item.php)
* drop (in diaspora.php, items.php, photos.php)
* edit_post (in item.php)
* event (in events.php)
* expire (in items.php)
* like (in like.php, poke.php)
* mail (in message.php)
* tag (in photos.php, poke.php, tagger.php)
* tgroup (in items.php)
* wall-new (in photos.php, item.php)
*
* and ITEM_ID is the id of the item in the database that needs to be sent to others.
*
* ZOT
* permissions_create abook_id
* permissions_accept abook_id
* permissions_reject abook_id
* permissions_update abook_id
* refresh_all channel_id
* purge xchan_hash
* purge_all channel_id
* expire channel_id
* relay item_id (item was relayed to owner, we will deliver it as owner)
* single_activity item_id (deliver to a singleton network from the appropriate clone)
* single_mail mail_id (deliver to a singleton network from the appropriate clone)
* location channel_id
* request channel_id xchan_hash message_id
* rating xlink_id
* keychange channel_id
*
*/
class Notifier
{
public static $deliveries = [];
public static $recipients = [];
public static $env_recips = [];
public static $packet_type = 'activity';
public static $encoding = 'activitystreams';
public static $encoded_item = null;
public static $channel = null;
public static $private = false;
public static function run($argc, $argv)
{
if ($argc < 3) {
return;
}
logger('notifier: invoked: ' . print_r($argv, true), LOGGER_DEBUG, LOG_INFO);
$cmd = $argv[1];
$item_id = $argv[2];
if (! $item_id) {
return;
}
self::$deliveries = [];
self::$recipients = [];
self::$env_recips = [];
self::$packet_type = 'activity';
self::$encoding = 'activitystreams';
self::$encoded_item = null;
self::$channel = null;
self::$private = false;
$sys = Channel::get_system();
$top_level = false;
$url_recipients = [];
$normal_mode = true;
if ($cmd === 'request') {
$xchan = $argv[3];
if ($argc < 5) {
return;
}
self::$channel = Channel::from_id($item_id);
self::$private = true;
self::$recipients[] = $xchan;
self::$packet_type = 'request';
self::$encoded_item = [ 'message_id' => $argv[4] ];
self::$encoding = 'zot';
$normal_mode = false;
} elseif ($cmd === 'keychange') {
self::$channel = Channel::from_id($item_id);
$r = q(
"select abook_xchan from abook where abook_channel = %d",
intval($item_id)
);
if ($r) {
foreach ($r as $rr) {
self::$recipients[] = $rr['abook_xchan'];
}
}
self::$private = false;
self::$packet_type = 'keychange';
self::$encoded_item = get_pconfig(self::$channel['channel_id'], 'system', 'keychange');
self::$encoding = 'zot';
$normal_mode = false;
} elseif (in_array($cmd, [ 'permissions_update', 'permissions_reject', 'permissions_accept', 'permissions_create' ])) {
// Get the (single) recipient
$r = q(
"select * from abook left join xchan on abook_xchan = xchan_hash where abook_id = %d and abook_self = 0",
intval($item_id)
);
if ($r) {
$recip = array_shift($r);
$uid = $recip['abook_channel'];
// Get the sender
self::$channel = Channel::from_id($uid);
if (self::$channel) {
$perm_update = [ 'sender' => self::$channel, 'recipient' => $recip, 'success' => false, 'deliveries' => '' ];
switch ($cmd) {
case 'permissions_create':
ActivityPub::permissions_create($perm_update);
break;
case 'permissions_accept':
ActivityPub::permissions_accept($perm_update);
break;
case 'permissions_update':
ActivityPub::permissions_update($perm_update);
break;
default:
break;
}
if (! $perm_update['success']) {
Hook::call($cmd, $perm_update);
}
if ($perm_update['success']) {
if ($perm_update['deliveries']) {
self::$deliveries[] = $perm_update['deliveries'];
do_delivery(self::$deliveries);
}
return;
} else {
self::$recipients[] = $recip['abook_xchan'];
self::$private = false;
self::$packet_type = 'refresh';
self::$env_recips = [ $recip['xchan_hash'] ];
}
}
}
} elseif ($cmd === 'refresh_all') {
logger('notifier: refresh_all: ' . $item_id);
self::$channel = Channel::from_id($item_id, true);
$r = q(
"select abook_xchan from abook where abook_channel = %d",
intval($item_id)
);
if ($r) {
foreach ($r as $rr) {
self::$recipients[] = $rr['abook_xchan'];
}
}
self::$recipients[] = self::$channel['channel_hash'];
self::$private = false;
self::$packet_type = 'refresh';
} elseif ($cmd === 'purge') {
$xchan = $argv[3];
logger('notifier: purge: ' . $item_id . ' => ' . $xchan);
if (! $xchan) {
return;
}
self::$channel = Channel::from_id($item_id, true);
self::$recipients = [ $xchan ];
self::$private = true;
self::$packet_type = 'purge';
} elseif ($cmd === 'purge_all') {
logger('notifier: purge_all: ' . $item_id);
self::$channel = Channel::from_id($item_id, true);
self::$recipients = [];
$r = q(
"select abook_xchan from abook where abook_channel = %d and abook_self = 0",
intval($item_id)
);
if (! $r) {
return;
}
foreach ($r as $rr) {
self::$recipients[] = $rr['abook_xchan'];
}
self::$private = false;
self::$packet_type = 'purge';
} else {
// Normal items
// Fetch the target item
$r = q(
"SELECT * FROM item WHERE id = %d and parent != 0 LIMIT 1",
intval($item_id)
);
if (! $r) {
return;
}
xchan_query($r);
$r = fetch_post_tags($r);
$target_item = array_shift($r);
if ($target_item['author']['xchan_network'] === 'anon') {
logger('notifier: target item author is not a fetchable actor', LOGGER_DEBUG);
return;
}
$deleted_item = false;
if (intval($target_item['item_deleted'])) {
logger('notifier: target item ITEM_DELETED', LOGGER_DEBUG);
$deleted_item = true;
}
if (! in_array(intval($target_item['item_type']), [ ITEM_TYPE_POST, ITEM_TYPE_MAIL ])) {
if (intval($target_item['item_type'] == ITEM_TYPE_CUSTOM)) {
$hookinfo=[
'targetitem' => $target_item,
'deliver' => false
];
Hook::call('customitem_deliver', $hookinfo);
}
if (! $hookinfo['deliver']) {
logger('notifier: target item not forwardable: type ' . $target_item['item_type'], LOGGER_DEBUG);
return;
}
}
// Check for non published items, but allow an exclusion for transmitting hidden file activities
if (intval($target_item['item_unpublished']) || intval($target_item['item_delayed']) ||
intval($target_item['item_blocked']) ||
( intval($target_item['item_hidden']) && ($target_item['obj_type'] !== ACTIVITY_OBJ_FILE))) {
logger('notifier: target item not published, so not forwardable', LOGGER_DEBUG);
return;
}
if (in_array($target_item['verb'], [ ACTIVITY_FOLLOW, ACTIVITY_IGNORE ])) {
logger('not fowarding follow|unfollow->note activity');
return;
}
$s = q(
"select * from channel left join xchan on channel_hash = xchan_hash where channel_id = %d limit 1",
intval($target_item['uid'])
);
if ($s) {
self::$channel = array_shift($s);
}
if (self::$channel['channel_hash'] !== $target_item['author_xchan'] && self::$channel['channel_hash'] !== $target_item['owner_xchan']) {
logger("notifier: Sending channel is not owner {$target_item['owner_xchan']} or author {$target_item['author_xchan']}", LOGGER_NORMAL, LOG_WARNING);
return;
}
$thread_is_public = false;
if ($target_item['mid'] === $target_item['parent_mid']) {
$parent_item = $target_item;
$top_level_post = true;
} else {
// fetch the parent item
$r = q(
"SELECT * from item where id = %d order by id asc",
intval($target_item['parent'])
);
if (! $r) {
return;
}
xchan_query($r);
$r = fetch_post_tags($r);
$parent_item = array_shift($r);
$top_level_post = false;
$thread_is_public = ((intval($parent_item['item_private'])) ? false : true) ;
}
// avoid looping of discover items 12/4/2014
if ($sys && $parent_item['uid'] == $sys['channel_id']) {
return;
}
$m = get_iconfig($target_item, 'activitypub', 'signed_data');
// Re-use existing signature unless the activity type changed to a Tombstone, which won't verify.
if ($m && (! intval($target_item['item_deleted']))) {
self::$encoded_item = json_decode($m, true);
} else {
self::$encoded_item = array_merge(['@context' => [
ACTIVITYSTREAMS_JSONLD_REV,
'https://w3id.org/security/v1',
Activity::ap_schema()
]], Activity::encode_activity($target_item, true));
self::$encoded_item['signature'] = LDSignatures::sign(self::$encoded_item, self::$channel);
}
logger('target_item: ' . print_r($target_item, true), LOGGER_DEBUG);
logger('encoded: ' . print_r(self::$encoded_item, true), LOGGER_DEBUG);
// Send comments to the owner to re-deliver to everybody in the conversation
// We only do this if the item in question originated on this site. This prevents looping.
// To clarify, a site accepting a new comment is responsible for sending it to the owner for relay.
// Relaying should never be initiated on a post that arrived from elsewhere.
// We should normally be able to rely on ITEM_ORIGIN, but start_delivery_chain() incorrectly set this
// flag on comments for an extended period. So we'll also call comment_local_origin() which looks at
// the hostname in the message_id and provides a second (fallback) opinion.
$relay_to_owner = (((! $top_level_post) && (intval($target_item['item_origin'])) && comment_local_origin($target_item) && $cmd !== 'hyper') ? true : false);
$uplink = false;
// $cmd === 'relay' indicates the owner is sending it to the original recipients
// don't allow the item in the relay command to relay to owner under any circumstances, it will loop
logger('notifier: relay_to_owner: ' . (($relay_to_owner) ? 'true' : 'false'), LOGGER_DATA, LOG_DEBUG);
logger('notifier: top_level_post: ' . (($top_level_post) ? 'true' : 'false'), LOGGER_DATA, LOG_DEBUG);
// tag_deliver'd post which needs to be sent back to the original author
if (($cmd === 'uplink') && intval($parent_item['item_uplink']) && (! $top_level_post)) {
logger('notifier: uplink');
$uplink = true;
self::$packet_type = 'response';
}
if (($relay_to_owner || $uplink) && ($cmd !== 'relay')) {
logger('followup relay (upstream delivery)', LOGGER_DEBUG);
$sendto = ($uplink) ? $parent_item['source_xchan'] : $parent_item['owner_xchan'];
self::$recipients = [ $sendto ];
// over-ride upstream recipients if 'replyTo' was set in the parent.
if ($parent_item['replyto'] && (! $uplink)) {
logger('replyto: over-riding owner ' . $sendto, LOGGER_DEBUG);
// unserialise is a no-op if presented with data that wasn't serialised.
$ptr = unserialise($parent_item['replyto']);
if (is_string($ptr)) {
if (ActivityStreams::is_url($sendto)) {
$sendto = $ptr;
self::$recipients = [ $sendto ];
}
} elseif (is_array($ptr)) {
$sendto = [];
foreach ($ptr as $rto) {
if (is_string($rto)) {
$sendto[] = $rto;
} elseif (is_array($rto) && isset($rto['id'])) {
$sendto[] = $rto['id'];
}
}
self::$recipients = $sendto;
}
}
logger('replyto: upstream recipients ' . print_r($sendto, true), LOGGER_DEBUG);
self::$private = true;
$upstream = true;
self::$packet_type = 'response';
$is_moderated = their_perms_contains($parent_item['uid'], $sendto, 'moderated');
if ($relay_to_owner && $thread_is_public && (! $is_moderated) && (! Channel::is_group($parent_item['uid']))) {
if (get_pconfig($target_item['uid'], 'system', 'hyperdrive', true)) {
Run::Summon([ 'Notifier' , 'hyper', $item_id ]);
}
}
} else {
if ($cmd === 'relay') {
logger('owner relay (downstream delivery)');
} else {
logger('normal (downstream) distribution', LOGGER_DEBUG);
}
$upstream = false;
if ($parent_item && $parent_item['item_private'] !== $target_item['item_private']) {
logger('parent_item: ' . $parent_item['id'] . ' item_private: ' . $parent_item['item_private']);
logger('target_item: ' . $target_item['id'] . ' item_private: ' . $target_item['item_private']);
logger('conversation privacy mismatch - downstream delivery prevented');
return;
}
// if our parent is a tag_delivery recipient, uplink to the original author causing
// a delivery fork.
if (($parent_item) && intval($parent_item['item_uplink']) && (! $top_level_post) && ($cmd !== 'uplink')) {
// don't uplink a relayed post to the relay owner
if ($parent_item['source_xchan'] !== $parent_item['owner_xchan']) {
logger('notifier: uplinking this item');
Run::Summon([ 'Notifier','uplink',$item_id ]);
}
}
if ($thread_is_public && $cmd === 'hyper') {
self::$recipients = [];
$r = q(
"select abook_xchan, xchan_network from abook left join xchan on abook_xchan = xchan_hash where abook_channel = %d and abook_self = 0 and abook_pending = 0 and abook_archived = 0 and not abook_xchan in ( '%s', '%s', '%s' ) ",
intval($target_item['uid']),
dbesc($target_item['author_xchan']),
dbesc($target_item['owner_xchan']),
dbesc($target_item['source_xchan'])
);
if ($r) {
foreach ($r as $rv) {
self::$recipients[] = $rv['abook_xchan'];
}
}
self::$private = false;
} else {
self::$private = false;
self::$recipients = collect_recipients($parent_item, self::$private);
}
// @FIXME add any additional recipients such as mentions, etc.
if ($top_level_post) {
// remove clones who will receive the post via sync
self::$recipients = array_values(array_diff(self::$recipients, [ $target_item['owner_xchan'] ]));
}
// don't send deletions onward for other people's stuff
if (intval($target_item['item_deleted']) && (! intval($target_item['item_wall']))) {
logger('notifier: ignoring delete notification for non-wall item', LOGGER_NORMAL, LOG_NOTICE);
return;
}
}
}
// Generic delivery section, we have an encoded item and recipients
// Now start the delivery process
logger('encoded item: ' . print_r(self::$encoded_item, true), LOGGER_DATA, LOG_DEBUG);
// This addresses an issue that crossposting addons weren't being called if the sender had no friends
// and only wanted to crosspost.
$crossposting = (isset($target_item['postopts']) && $target_item['postopts']) ? true : false;
stringify_array_elms(self::$recipients);
if ( (! self::$recipients && ! $crossposting)) {
logger('no recipients');
return;
}
// logger('recipients: ' . print_r(self::$recipients,true), LOGGER_NORMAL, LOG_DEBUG);
if (! count(self::$env_recips)) {
self::$env_recips = ((self::$private) ? [] : null);
}
$recip_list = [];
if (self::$recipients) {
$details = q("select xchan_hash, xchan_network, xchan_addr, xchan_guid, xchan_guid_sig from xchan
where xchan_hash in (" . protect_sprintf(implode(',', self::$recipients)) . ")");
}
else {
$details = [];
}
if ($details) {
foreach ($details as $d) {
$recip_list[] = $d['xchan_addr'] . ' (' . $d['xchan_hash'] . ')';
if (self::$private) {
self::$env_recips[] = $d['xchan_hash'];
}
}
}
$narr = [
'channel' => self::$channel,
'upstream' => $upstream,
'env_recips' => self::$env_recips,
'recipients' => self::$recipients,
'item' => $item,
'target_item' => $target_item,
'parent_item' => $parent_item,
'top_level_post' => $top_level_post,
'private' => self::$private,
'relay_to_owner' => $relay_to_owner,
'uplink' => $uplink,
'cmd' => $cmd,
'single' => (($cmd === 'single_activity') ? true : false),
'request' => $request,
'normal_mode' => $normal_mode,
'packet_type' => self::$packet_type,
'queued' => []
];
Hook::call('notifier_process', $narr);
if ($narr['queued']) {
foreach ($narr['queued'] as $pq) {
self::$deliveries[] = $pq;
}
}
// notifier_process can alter the recipient list
self::$recipients = $narr['recipients'];
self::$env_recips = $narr['env_recips'];
if ((self::$private) && (! self::$env_recips)) {
// shouldn't happen
logger('private message with no envelope recipients.' . print_r($argv, true), LOGGER_NORMAL, LOG_NOTICE);
return;
}
logger('notifier: recipients (may be delivered to more if public): ' . print_r($recip_list, true), LOGGER_DEBUG);
// Now we have collected recipients (except for external mentions, @FIXME)
// Let's reduce this to a set of hubs; checking that the site is not dead.
if (self::$recipients) {
$hubs = q("select hubloc.*, site.site_crypto, site.site_flags from hubloc left join site on site_url = hubloc_url
where hubloc_hash in (" . protect_sprintf(implode(',', self::$recipients)) . ")
and hubloc_error = 0 and hubloc_deleted = 0 ");
}
else {
$hubs = [];
}
// public posts won't make it to the local public stream unless there's a recipient on this site.
// This code block sees if it's a public post and localhost is missing, and if so adds an entry for the local sys channel to the $hubs list
if (! self::$private) {
$found_localhost = false;
if ($hubs) {
foreach ($hubs as $h) {
if ($h['hubloc_url'] === z_root()) {
$found_localhost = true;
break;
}
}
}
if (! $found_localhost) {
$localhub = q(
"select hubloc.*, site.site_crypto, site.site_flags, site.site_dead from hubloc
left join site on site_url = hubloc_url where hubloc_id_url = '%s' and hubloc_error = 0 and hubloc_deleted = 0 ",
dbesc(z_root() . '/channel/sys')
);
if ($localhub) {
$hubs = array_merge($hubs, $localhub);
}
}
}
if (! $hubs) {
logger('notifier: no hubs', LOGGER_NORMAL, LOG_NOTICE);
return;
}
/**
* Reduce the hubs to those that are unique. For zot hubs, we need to verify uniqueness by the sitekey,
* since it may have been a re-install which has not yet been detected and pruned.
* For other networks which don't have or require sitekeys, we'll have to use the URL
*/
$hublist = []; // this provides an easily printable list for the logs
$dhubs = []; // delivery hubs where we store our resulting unique array
$keys = []; // array of keys to check uniquness for zot hubs
$urls = []; // array of urls to check uniqueness of hubs from other networks
$hub_env = []; // per-hub envelope so we don't broadcast the entire envelope to all
$dead = []; // known dead hubs - report them as undeliverable
foreach ($hubs as $hub) {
if (isset($hub['site_dead']) && intval($hub['site_dead'])) {
$dead[] = $hub;
continue;
}
if (self::$env_recips) {
foreach (self::$env_recips as $er) {
if ($hub['hubloc_hash'] === $er) {
if (! array_key_exists($hub['hubloc_site_id'], $hub_env)) {
$hub_env[$hub['hubloc_site_id']] = [];
}
$hub_env[$hub['hubloc_site_id']][] = $er;
}
}
}
if (in_array($hub['hubloc_network'],['nomad','zot6'])) {
if (! in_array($hub['hubloc_sitekey'], $keys)) {
$hublist[] = $hub['hubloc_host'] . ' ' . $hub['hubloc_network'];
$dhubs[] = $hub;
$keys[] = $hub['hubloc_sitekey'];
}
} else {
if (! in_array($hub['hubloc_url'], $urls)) {
$hublist[] = $hub['hubloc_host'] . ' ' . $hub['hubloc_network'];
$dhubs[] = $hub;
$urls[] = $hub['hubloc_url'];
}
}
}
logger('notifier: will notify/deliver to these hubs: ' . print_r($hublist, true), LOGGER_DEBUG, LOG_DEBUG);
foreach ($dhubs as $hub) {
logger('notifier_hub: ' . $hub['hubloc_url'], LOGGER_DEBUG, LOG_DEBUG);
// deliver to any non-zot networks
if (! in_array($hub['hubloc_network'], ['zot6', 'nomad' ])) {
$narr = [
'channel' => self::$channel,
'upstream' => $upstream,
'env_recips' => self::$env_recips,
'recipients' => self::$recipients,
'item' => $item,
'target_item' => $target_item,
'parent_item' => $parent_item,
'hub' => $hub,
'top_level_post' => $top_level_post,
'private' => self::$private,
'relay_to_owner' => $relay_to_owner,
'uplink' => $uplink,
'cmd' => $cmd,
'single' => (($cmd === 'single_activity') ? true : false),
'request' => $request,
'normal_mode' => $normal_mode,
'packet_type' => self::$packet_type,
'queued' => []
];
ActivityPub::notifier_process($narr);
Hook::call('notifier_hub', $narr);
if ($narr['queued']) {
foreach ($narr['queued'] as $pq) {
self::$deliveries[] = $pq;
}
}
continue;
}
// Single deliveries are for non-nomadic federated networks and we're essentially
// delivering only to those that have this site url in their abook_instance
// and only from within a sync operation. This means if you post from a clone,
// and a connection is connected to one of your other clones; assuming that hub
// is running it will receive a sync packet. On receipt of this sync packet it
// will invoke a delivery to those connections which are connected to just that
// hub instance.
if ($cmd === 'single_activity') {
continue;
}
// default: zot or nomad protocol
// Prevent zot6/Nomad delivery of group comment boosts, which are not required for conversational platforms.
// ActivityPub conversational platforms may wish to filter these if they don't want or require them.
// We will assume here that if $target_item exists and has a verb that it is an actual item structure
// so we won't need to check the existence of the other item fields prior to evaluation.
// This shouldn't produce false positives on comment boosts that were generated on other platforms
// because we won't be delivering them.
if (isset($target_item) && isset($target_item['verb']) && $target_item['verb'] === 'Announce' && $target_item['author_xchan'] === $target_item['owner_xchan'] && ! intval($target_item['item_thread_top'])) {
continue;
}
$hash = new_uuid();
$env = (($hub_env && $hub_env[$hub['hubloc_site_id']]) ? $hub_env[$hub['hubloc_site_id']] : '');
if ((self::$private) && (! $env)) {
continue;
}
$packet = Libzot::build_packet(
self::$channel,
self::$packet_type,
$env,
self::$encoded_item,
self::$encoding,
((self::$private) ? $hub['hubloc_sitekey'] : null),
$hub['site_crypto']
);
Queue::insert(
[
'hash' => $hash,
'account_id' => self::$channel['channel_account_id'],
'channel_id' => self::$channel['channel_id'],
'posturl' => $hub['hubloc_callback'],
'driver' => $hub['hubloc_network'],
'notify' => $packet,
'msg' => EMPTY_STR
]
);
// only create delivery reports for normal undeleted items
if (is_array($target_item) && (! $target_item['item_deleted']) && (! get_config('system', 'disable_dreport'))) {
q(
"insert into dreport ( dreport_mid, dreport_site, dreport_recip, dreport_name, dreport_result, dreport_time, dreport_xchan, dreport_queue, dreport_log )
values ( '%s', '%s','%s','%s','%s','%s','%s','%s', '%s' ) ",
dbesc($target_item['mid']),
dbesc($hub['hubloc_host']),
dbesc($hub['hubloc_host']),
dbesc($hub['hubloc_host']),
dbesc('queued'),
dbesc(datetime_convert()),
dbesc(self::$channel['channel_hash']),
dbesc($hash),
dbesc(EMPTY_STR)
);
}
self::$deliveries[] = $hash;
}
if ($normal_mode) {
// This wastes a process if there are no delivery hooks configured, so check this before launching the new process
$x = q("select * from hook where hook = 'notifier_normal'");
if ($x) {
Run::Summon([ 'Deliver_hooks', $target_item['id'] ]);
}
}
if (self::$deliveries) {
do_delivery(self::$deliveries);
}
if ($dead) {
foreach ($dead as $deceased) {
if (is_array($target_item) && (! $target_item['item_deleted']) && (! get_config('system', 'disable_dreport'))) {
q(
"insert into dreport ( dreport_mid, dreport_site, dreport_recip, dreport_name, dreport_result, dreport_time, dreport_xchan, dreport_queue, dreport_log )
values ( '%s', '%s','%s','%s','%s','%s','%s','%s','%s' ) ",
dbesc($target_item['mid']),
dbesc($deceased['hubloc_host']),
dbesc($deceased['hubloc_host']),
dbesc($deceased['hubloc_host']),
dbesc('undeliverable/unresponsive site'),
dbesc(datetime_convert()),
dbesc(self::$channel['channel_hash']),
dbesc(new_uuid()),
dbesc(EMPTY_STR)
);
}
}
}
Hook::call('notifier_end', $target_item);
logger('notifer: complete.');
return;
}
}

View file

@ -0,0 +1,87 @@
<?php
/** @file */
namespace Code\Daemon;
use Code\Lib\Libzotdir;
class Onedirsync
{
public static function run($argc, $argv)
{
logger('onedirsync: start ' . intval($argv[1]));
if (($argc > 1) && (intval($argv[1]))) {
$update_id = intval($argv[1]);
}
if (! $update_id) {
logger('onedirsync: no update');
return;
}
$r = q(
"select * from updates where ud_id = %d limit 1",
intval($update_id)
);
if (! $r) {
return;
}
if (($r[0]['ud_flags'] & UPDATE_FLAGS_UPDATED) || (! $r[0]['ud_addr'])) {
return;
}
// Have we probed this channel more recently than the other directory server
// (where we received this update from) ?
// If we have, we don't need to do anything except mark any older entries updated
$x = q(
"select * from updates where ud_addr = '%s' and ud_date > '%s' and ( ud_flags & %d )>0 order by ud_date desc limit 1",
dbesc($r[0]['ud_addr']),
dbesc($r[0]['ud_date']),
intval(UPDATE_FLAGS_UPDATED)
);
if ($x) {
$y = q(
"update updates set ud_flags = ( ud_flags | %d ) where ud_addr = '%s' and ( ud_flags & %d ) = 0 and ud_date != '%s'",
intval(UPDATE_FLAGS_UPDATED),
dbesc($r[0]['ud_addr']),
intval(UPDATE_FLAGS_UPDATED),
dbesc($x[0]['ud_date'])
);
return;
}
// ignore doing an update if this ud_addr refers to a known dead hubloc
$h = q(
"select * from hubloc where hubloc_addr = '%s' limit 1",
dbesc($r[0]['ud_addr'])
);
if (($h) && ($h[0]['hubloc_status'] & HUBLOC_OFFLINE)) {
$y = q(
"update updates set ud_flags = ( ud_flags | %d ) where ud_addr = '%s' and ( ud_flags & %d ) = 0 ",
intval(UPDATE_FLAGS_UPDATED),
dbesc($r[0]['ud_addr']),
intval(UPDATE_FLAGS_UPDATED)
);
return;
}
// we might have to pull this out some day, but for now update_directory_entry()
// runs zot_finger() and is kind of zot specific
if ($h && in_array($h[0]['hubloc_network'],['nomad','zot6'])) {
return;
}
Libzotdir::update_directory_entry($r[0]);
return;
}
}

178
Code/Daemon/Onepoll.php Normal file
View file

@ -0,0 +1,178 @@
<?php
/** @file */
namespace Code\Daemon;
use Code\Lib\Libzot;
use Code\Lib\ActivityStreams;
use Code\Lib\Activity;
use Code\Lib\ASCollection;
use Code\Lib\Socgraph;
class Onepoll
{
public static function run($argc, $argv)
{
logger('onepoll: start');
if (($argc > 1) && (intval($argv[1]))) {
$contact_id = intval($argv[1]);
}
if (! $contact_id) {
logger('onepoll: no contact');
return;
}
$d = datetime_convert();
$contacts = q(
"SELECT abook.*, xchan.*, account.*
FROM abook LEFT JOIN account on abook_account = account_id left join xchan on xchan_hash = abook_xchan
where abook_id = %d
and abook_pending = 0 and abook_archived = 0 and abook_blocked = 0 and abook_ignored = 0
AND (( account_flags = %d ) OR ( account_flags = %d )) limit 1",
intval($contact_id),
intval(ACCOUNT_OK),
intval(ACCOUNT_UNVERIFIED)
);
if (! $contacts) {
logger('onepoll: abook_id not found: ' . $contact_id);
return;
}
$contact = array_shift($contacts);
$t = $contact['abook_updated'];
$importer_uid = $contact['abook_channel'];
$r = q(
"SELECT * from channel left join xchan on channel_hash = xchan_hash where channel_id = %d limit 1",
intval($importer_uid)
);
if (! $r) {
return;
}
$importer = $r[0];
logger("onepoll: poll: ({$contact['id']}) IMPORTER: {$importer['xchan_name']}, CONTACT: {$contact['xchan_name']}");
$last_update = ((($contact['abook_updated'] === $contact['abook_created']) || ($contact['abook_updated'] <= NULL_DATE))
? datetime_convert('UTC', 'UTC', 'now - 7 days')
: datetime_convert('UTC', 'UTC', $contact['abook_updated'] . ' - 2 days')
);
if (in_array($contact['xchan_network'],['nomad']['zot6'])) {
// update permissions
$x = Libzot::refresh($contact, $importer);
$responded = false;
$updated = datetime_convert();
$connected = datetime_convert();
if (! $x) {
// mark for death by not updating abook_connected, this is caught in include/poller.php
q(
"update abook set abook_updated = '%s' where abook_id = %d",
dbesc($updated),
intval($contact['abook_id'])
);
} else {
q(
"update abook set abook_updated = '%s', abook_connected = '%s' where abook_id = %d",
dbesc($updated),
dbesc($connected),
intval($contact['abook_id'])
);
$responded = true;
}
if (! $responded) {
return;
}
}
$fetch_feed = true;
// They haven't given us permission to see their stream
$can_view_stream = intval(get_abconfig($importer_uid, $contact['abook_xchan'], 'their_perms', 'view_stream'));
if (! $can_view_stream) {
$fetch_feed = false;
}
// we haven't given them permission to send us their stream
$can_send_stream = intval(get_abconfig($importer_uid, $contact['abook_xchan'], 'my_perms', 'send_stream'));
if (! $can_send_stream) {
$fetch_feed = false;
}
if ($contact['abook_created'] < datetime_convert('UTC', 'UTC', 'now - 1 week')) {
$fetch_feed = false;
}
// In previous releases there was a mechanism to fetch 'external' or public stream posts from a site
// (as opposed to a channel). This mechanism was deprecated as there is no reliable/scalable method
// for informing downstream publishers when/if the content has expired or been deleted.
// We can use the ThreadListener interface to implement this on the owner's outbox, however this is still a
// work in progress and may present scaling issues. Making this work correctly with third-party fetches is
// prohibitive as deletion requests would need to be relayed over potentially hostile networks.
if ($fetch_feed) {
$max = intval(get_config('system', 'max_imported_posts', 20));
if (intval($max)) {
$cl = get_xconfig($xchan, 'activitypub', 'collections');
if (is_array($cl) && $cl) {
$url = ((array_key_exists('outbox', $cl)) ? $cl['outbox'] : '');
if ($url) {
logger('fetching outbox');
$url = $url . '?date_begin=' . urlencode($last_update);
$obj = new ASCollection($url, $importer, 0, $max);
$messages = $obj->get();
if ($messages) {
foreach ($messages as $message) {
if (is_string($message)) {
$message = Activity::fetch($message, $importer);
}
if (is_array($message)) {
$AS = new ActivityStreams($message, null, true);
if ($AS->is_valid() && is_array($AS->obj)) {
$item = Activity::decode_note($AS, true);
if ($item) {
Activity::store($importer, $contact['abook_xchan'], $AS, $item, true, true);
}
}
}
}
}
}
}
}
}
// update the poco details for this connection
$r = q(
"SELECT xlink_id from xlink
where xlink_xchan = '%s' and xlink_updated > %s - INTERVAL %s and xlink_static = 0 limit 1",
intval($contact['xchan_hash']),
db_utcnow(),
db_quoteinterval('7 DAY')
);
if (! $r) {
Socgraph::poco_load($contact['xchan_hash'], $contact['xchan_connurl']);
}
return;
}
}

206
Code/Daemon/Poller.php Normal file
View file

@ -0,0 +1,206 @@
<?php
/** @file */
namespace Code\Daemon;
use Code\Lib\ServiceClass;
use Code\Lib\Addon;
class Poller
{
public static function run($argc, $argv)
{
$maxsysload = intval(get_config('system', 'maxloadavg'));
if ($maxsysload < 1) {
$maxsysload = 50;
}
if (function_exists('sys_getloadavg')) {
$load = sys_getloadavg();
if (intval($load[0]) > $maxsysload) {
logger('system: load ' . $load . ' too high. Poller deferred to next scheduled run.');
return;
}
}
$interval = intval(get_config('system', 'poll_interval'));
if (! $interval) {
$interval = ((get_config('system', 'delivery_interval') === false) ? 3 : intval(get_config('system', 'delivery_interval')));
}
// Check for a lockfile. If it exists, but is over an hour old, it's stale. Ignore it.
$lockfile = 'cache/poller';
if (
(file_exists($lockfile)) && (filemtime($lockfile) > (time() - 3600))
&& (! get_config('system', 'override_poll_lockfile'))
) {
logger("poller: Already running");
return;
}
// Create a lockfile.
file_put_contents($lockfile, EMPTY_STR);
logger('poller: start');
$manual_id = 0;
$generation = 0;
$force = false;
$restart = false;
if (($argc > 1) && ($argv[1] == 'force')) {
$force = true;
}
if (($argc > 1) && ($argv[1] == 'restart')) {
$restart = true;
$generation = intval($argv[2]);
if (! $generation) {
return;
}
}
if (($argc > 1) && intval($argv[1])) {
$manual_id = intval($argv[1]);
$force = true;
}
$sql_extra = (($manual_id) ? " AND abook_id = " . intval($manual_id) . " " : "");
Addon::reload_all();
$d = datetime_convert();
// Only poll from those with suitable relationships
// $abandon_sql = (($abandon_days)
// ? sprintf(" AND account_lastlog > %s - INTERVAL %s ", db_utcnow(), db_quoteinterval(intval($abandon_days).' DAY'))
// : ''
// );
$abandon_sql = EMPTY_STR;
$randfunc = db_getfunc('RAND');
$contacts = q(
"SELECT abook.abook_updated, abook.abook_connected, abook.abook_feed,
abook.abook_channel, abook.abook_id, abook.abook_archived, abook.abook_pending,
abook.abook_ignored, abook.abook_blocked,
xchan.xchan_network,
account.account_lastlog, account.account_flags
FROM abook LEFT JOIN xchan on abook_xchan = xchan_hash
LEFT JOIN account on abook_account = account_id
where abook_self = 0
$sql_extra
AND (( account_flags = %d ) OR ( account_flags = %d )) $abandon_sql ORDER BY $randfunc",
intval(ACCOUNT_OK),
intval(ACCOUNT_UNVERIFIED) // FIXME
);
if ($contacts) {
foreach ($contacts as $contact) {
$update = false;
$t = $contact['abook_updated'];
$c = $contact['abook_connected'];
if (intval($contact['abook_feed'])) {
$min = ServiceClass::fetch($contact['abook_channel'], 'minimum_feedcheck_minutes');
if (! $min) {
$min = intval(get_config('system', 'minimum_feedcheck_minutes'));
}
if (! $min) {
$min = 60;
}
$x = datetime_convert('UTC', 'UTC', "now - $min minutes");
if ($c < $x) {
Run::Summon([ 'Onepoll', $contact['abook_id'] ]);
if ($interval) {
@time_sleep_until(microtime(true) + (float) $interval);
}
}
continue;
}
if (! in_array($contact['xchan_network'],['nomad','zot6'])) {
continue;
}
if ($c == $t) {
if (datetime_convert('UTC', 'UTC', 'now') > datetime_convert('UTC', 'UTC', $t . " + 1 day")) {
$update = true;
}
} else {
// if we've never connected with them, start the mark for death countdown from now
if ($c <= NULL_DATE) {
$r = q(
"update abook set abook_connected = '%s' where abook_id = %d",
dbesc(datetime_convert()),
intval($contact['abook_id'])
);
$c = datetime_convert();
$update = true;
}
// He's dead, Jim
if (strcmp(datetime_convert('UTC', 'UTC', 'now'), datetime_convert('UTC', 'UTC', $c . " + 30 day")) > 0) {
$r = q(
"update abook set abook_archived = 1 where abook_id = %d",
intval($contact['abook_id'])
);
$update = false;
continue;
}
if (intval($contact['abook_archived'])) {
$update = false;
continue;
}
// might be dead, so maybe don't poll quite so often
// recently deceased, so keep up the regular schedule for 3 days
if (
(strcmp(datetime_convert('UTC', 'UTC', 'now'), datetime_convert('UTC', 'UTC', $c . " + 3 day")) > 0)
&& (strcmp(datetime_convert('UTC', 'UTC', 'now'), datetime_convert('UTC', 'UTC', $t . " + 1 day")) > 0)
) {
$update = true;
}
// After that back off and put them on a morphine drip
if (strcmp(datetime_convert('UTC', 'UTC', 'now'), datetime_convert('UTC', 'UTC', $t . " + 2 day")) > 0) {
$update = true;
}
}
if (intval($contact['abook_pending']) || intval($contact['abook_archived']) || intval($contact['abook_ignored']) || intval($contact['abook_blocked'])) {
continue;
}
if ((! $update) && (! $force)) {
continue;
}
Run::Summon([ 'Onepoll',$contact['abook_id'] ]);
if ($interval) {
@time_sleep_until(microtime(true) + (float) $interval);
}
}
}
set_config('system', 'lastpoll', datetime_convert());
//All done - clear the lockfile
@unlink($lockfile);
return;
}
}

94
Code/Daemon/Queue.php Normal file
View file

@ -0,0 +1,94 @@
<?php
/** @file */
namespace Code\Daemon;
use Code\Lib as Zlib;
class Queue
{
public static function run($argc, $argv)
{
if ($argc > 1) {
$queue_id = $argv[1];
} else {
$queue_id = EMPTY_STR;
}
logger('queue: start');
// delete all queue items more than 3 days old
// but first mark these sites dead if we haven't heard from them in a month
$r = q(
"select outq_posturl from outq where outq_created < %s - INTERVAL %s",
db_utcnow(),
db_quoteinterval('3 DAY')
);
if ($r) {
foreach ($r as $rr) {
$site_url = '';
$h = parse_url($rr['outq_posturl']);
$desturl = $h['scheme'] . '://' . $h['host'] . (($h['port']) ? ':' . $h['port'] : '');
q(
"update site set site_dead = 1 where site_dead = 0 and site_url = '%s' and site_update < %s - INTERVAL %s",
dbesc($desturl),
db_utcnow(),
db_quoteinterval('1 MONTH')
);
}
}
$r = q(
"DELETE FROM outq WHERE outq_created < %s - INTERVAL %s",
db_utcnow(),
db_quoteinterval('3 DAY')
);
if ($queue_id) {
$r = q(
"SELECT * FROM outq WHERE outq_hash = '%s' LIMIT 1",
dbesc($queue_id)
);
} else {
// For the first 12 hours we'll try to deliver every 15 minutes
// After that, we'll only attempt delivery once per hour.
// This currently only handles the default queue drivers ('zot' or '') which we will group by posturl
// so that we don't start off a thousand deliveries for a couple of dead hubs.
// The zot driver will deliver everything destined for a single hub once contact is made (*if* contact is made).
// Other drivers will have to do something different here and may need their own query.
// Note: this requires some tweaking as new posts to long dead hubs once a day will keep them in the
// "every 15 minutes" category. We probably need to prioritise them when inserted into the queue
// or just prior to this query based on recent and long-term delivery history. If we have good reason to believe
// the site is permanently down, there's no reason to attempt delivery at all, or at most not more than once
// or twice a day.
$sqlrandfunc = db_getfunc('rand');
$r = q(
"SELECT *,$sqlrandfunc as rn FROM outq WHERE outq_delivered = 0 and outq_scheduled < %s order by rn limit 1",
db_utcnow()
);
while ($r) {
foreach ($r as $rv) {
Zlib\Queue::deliver($rv);
}
$r = q(
"SELECT *,$sqlrandfunc as rn FROM outq WHERE outq_delivered = 0 and outq_scheduled < %s order by rn limit 1",
db_utcnow()
);
}
}
if (! $r) {
return;
}
foreach ($r as $rv) {
Zlib\Queue::deliver($rv);
}
}
}

43
Code/Daemon/README.md Normal file
View file

@ -0,0 +1,43 @@
Daemon (background) Processes
=============================
This directory provides background tasks which are executed by a
command-line process and detached from normal web processing.
Background tasks are invoked by calling
Code\Daemon\Run::Summon([ $cmd, $arg1, $argn... ]);
The Run class loads the desired command file and passes the arguments.
To create a background task 'Foo' use the following template.
<?php
namespace Code\Daemon;
class Foo {
static public function run($argc,$argv) {
// do something
}
}
The Run class "summons" the command by creating an executable script
from the provided arguments, then it invokes "Release" to execute the script
detached from web processing. This process calls the static::run() function
with any command line arguments using the traditional argc, argv format.
Please note: These are *real* $argc, $argv variables passed from the command
line, and not the parsed argc() and argv() functions/variables which were
obtained from parsing path components of the request URL by web processes.
Background processes do not emit displayable output except through logs. They
should also not make any assumptions about their HTML and web environment
(as they do not have a web environment), particularly with respect to global
variables such as $_SERVER, $_REQUEST, $_GET, $_POST, $_COOKIES, and $_SESSION.

79
Code/Daemon/Run.php Normal file
View file

@ -0,0 +1,79 @@
<?php
namespace Code\Daemon;
use Code\Extend\Hook;
if (array_search(__file__, get_included_files()) === 0) {
require_once('include/cli_startup.php');
array_shift($argv);
$argc = count($argv);
if ($argc) {
Run::Release($argc, $argv);
}
return;
}
class Run
{
// These processes should be ignored by addons which enforce timeouts (e.g. queueworker)
// as it could result in corrupt data. Please add additional long running tasks to this list as they arise.
// Ideally the queueworker should probably be provided an allow list rather than a deny list as it will be easier
// to maintain. This was a quick hack to fix truncation of very large synced files when the queueworker addon is installed.
public static $long_running = [ 'Addon', 'Channel_purge', 'Checksites', 'Content_importer', 'Convo',
'Cron', 'Cron_daily', 'Cron_weekly', 'Delxitems', 'Expire', 'File_importer', 'Importfile'
];
public static function Summon($arr)
{
if (file_exists('maintenance_lock') || file_exists('cache/maintenance_lock')) {
return;
}
$hookinfo = [
'argv' => $arr,
'long_running' => self::$long_running
];
Hook::call('daemon_summon', $hookinfo);
$arr = $hookinfo['argv'];
$argc = count($arr);
if ((! is_array($arr) || ($argc < 1))) {
logger("Summon handled by hook.", LOGGER_DEBUG);
return;
}
proc_run('php', 'Code/Daemon/Run.php', $arr);
}
public static function Release($argc, $argv)
{
cli_startup();
$hookinfo = [
'argv' => $argv,
'long_running' => self::$long_running
];
Hook::call('daemon_release', $hookinfo);
$argv = $hookinfo['argv'];
$argc = count($argv);
if ((! is_array($argv) || ($argc < 1))) {
logger("Release handled by hook.", LOGGER_DEBUG);
return;
}
logger('Run: release: ' . print_r($argv, true), LOGGER_ALL, LOG_DEBUG);
$cls = '\\Code\\Daemon\\' . $argv[0];
$cls::run($argc, $argv);
}
}

87
Code/Daemon/Thumbnail.php Normal file
View file

@ -0,0 +1,87 @@
<?php
/** @file */
namespace Code\Daemon;
use Code\Extend\Hook;
class Thumbnail
{
public static function run($argc, $argv)
{
if (! ($argc == 2)) {
return;
}
$c = q(
"select * from attach where hash = '%s' ",
dbesc($argv[1])
);
if (! $c) {
return;
}
$attach = $c[0];
$preview_style = intval(get_config('system', 'thumbnail_security', 0));
$preview_width = intval(get_config('system', 'thumbnail_width', 300));
$preview_height = intval(get_config('system', 'thumbnail_height', 300));
$p = [
'attach' => $attach,
'preview_style' => $preview_style,
'preview_width' => $preview_width,
'preview_height' => $preview_height,
'thumbnail' => null
];
/**
* @hooks thumbnail
* * \e array \b attach
* * \e int \b preview_style
* * \e int \b preview_width
* * \e int \b preview_height
* * \e string \b thumbnail
*/
Hook::call('thumbnail', $p);
if ($p['thumbnail']) {
return;
}
$default_controller = null;
$files = glob('Code/Thumbs/*.php');
if ($files) {
foreach ($files as $f) {
$clsname = '\\Code\\Thumbs\\' . ucfirst(basename($f, '.php'));
if (class_exists($clsname)) {
$x = new $clsname();
if (method_exists($x, 'Match')) {
$matched = $x->Match($attach['filetype']);
if ($matched) {
$x->Thumb($attach, $preview_style, $preview_width, $preview_height);
}
}
if (method_exists($x, 'MatchDefault')) {
$default_matched = $x->MatchDefault(substr($attach['filetype'], 0, strpos($attach['filetype'], '/')));
if ($default_matched) {
$default_controller = $x;
}
}
}
}
}
if (
($default_controller)
&& ((! file_exists(dbunescbin($attach['content']) . '.thumb'))
|| (filectime(dbunescbin($attach['content']) . 'thumb') < (time() - 60)))
) {
$default_controller->Thumb($attach, $preview_style, $preview_width, $preview_height);
}
}
}

View file

@ -0,0 +1,39 @@
<?php
/** @file */
namespace Code\Daemon;
class Xchan_photo
{
public static function run($argc, $argv)
{
if ($argc != 3) {
return;
}
$url = hex2bin($argv[1]);
$xchan = hex2bin($argv[2]);
// Some photo sources hang after connect and aren't caught by curl timeout
set_time_limit(90);
$photos = import_remote_xchan_photo($url, $xchan);
if ($photos) {
$r = q(
"update xchan set xchan_photo_date = '%s', xchan_photo_l = '%s', xchan_photo_m = '%s', xchan_photo_s = '%s', xchan_photo_mimetype = '%s' where xchan_hash = '%s'",
dbescdate(datetime_convert()),
dbesc($photos[0]),
dbesc($photos[1]),
dbesc($photos[2]),
dbesc($photos[3]),
dbesc($xchan)
);
}
return;
}
}

236
Code/Extend/Hook.php Normal file
View file

@ -0,0 +1,236 @@
<?php
namespace Code\Extend;
use App;
/**
* @brief Hook class.
*
*/
class Hook
{
public static function register($hook, $file, $function, $version = 1, $priority = 0)
{
if (is_array($function)) {
$function = serialize($function);
}
$r = q(
"SELECT * FROM hook WHERE hook = '%s' AND file = '%s' AND fn = '%s' and priority = %d and hook_version = %d LIMIT 1",
dbesc($hook),
dbesc($file),
dbesc($function),
intval($priority),
intval($version)
);
if ($r) {
return true;
}
// To aid in upgrade and transition, remove old settings for any registered hooks that match in all respects except
// for priority or hook_version
$r = q(
"DELETE FROM hook where hook = '%s' and file = '%s' and fn = '%s'",
dbesc($hook),
dbesc($file),
dbesc($function)
);
$r = q(
"INSERT INTO hook (hook, file, fn, priority, hook_version) VALUES ( '%s', '%s', '%s', %d, %d )",
dbesc($hook),
dbesc($file),
dbesc($function),
intval($priority),
intval($version)
);
return $r;
}
public static function register_array($file, $arr)
{
if ($arr) {
foreach ($arr as $k => $v) {
self::register($k, $file, $v);
}
}
}
public static function unregister($hook, $file, $function, $version = 1, $priority = 0)
{
if (is_array($function)) {
$function = serialize($function);
}
$r = q(
"DELETE FROM hook WHERE hook = '%s' AND file = '%s' AND fn = '%s' and priority = %d and hook_version = %d",
dbesc($hook),
dbesc($file),
dbesc($function),
intval($priority),
intval($version)
);
return $r;
}
/**
* @brief Unregister all hooks with this file component.
*
* Useful for addon upgrades where you want to clean out old interfaces.
*
* @param string $file
*/
public static function unregister_by_file($file)
{
$r = q(
"DELETE FROM hook WHERE file = '%s' ",
dbesc($file)
);
return $r;
}
/**
* @brief Inserts a hook into a page request.
*
* Insert a short-lived hook into the running page request.
* Hooks are normally persistent so that they can be called
* across asynchronous processes such as delivery and poll
* processes.
*
* insert_hook lets you attach a hook callback immediately
* which will not persist beyond the life of this page request
* or the current process.
*
* @param string $hook
* name of hook to attach callback
* @param string $fn
* function name of callback handler
* @param int $version
* hook interface version, 0 uses two callback params, 1 uses one callback param
* @param int $priority
* currently not implemented in this function, would require the hook array to be resorted
*/
public static function insert($hook, $fn, $version = 0, $priority = 0)
{
if (is_array($fn)) {
$fn = serialize($fn);
}
if (! is_array(App::$hooks)) {
App::$hooks = [];
}
if (! array_key_exists($hook, App::$hooks)) {
App::$hooks[$hook] = [];
}
App::$hooks[$hook][] = [ '', $fn, $priority, $version ];
}
/**
* @brief loads all active hooks into memory
* alters: App::$hooks
* Called during initialisation
* Duplicated hooks are removed and the duplicates ignored
*
* It might not be obvious but themes can manually add hooks to the App::$hooks
* array in their theme_init() and use this to customise the app behaviour.
* use insert_hook($hookname,$function_name) to do this.
*/
public static function load()
{
App::$hooks = [];
$r = q("SELECT * FROM hook WHERE true ORDER BY priority DESC");
if ($r) {
foreach ($r as $rv) {
$duplicated = false;
if (! array_key_exists($rv['hook'], App::$hooks)) {
App::$hooks[$rv['hook']] = [];
} else {
foreach (App::$hooks[$rv['hook']] as $h) {
if ($h[0] === $rv['file'] && $h[1] === $rv['fn']) {
$duplicated = true;
q(
"delete from hook where id = %d",
intval($rv['id'])
);
logger('duplicate hook ' . $h[1] . ' removed');
}
}
}
if (! $duplicated) {
App::$hooks[$rv['hook']][] = [ $rv['file'], $rv['fn'], $rv['priority'], $rv['hook_version']];
}
}
}
// logger('hooks: ' . print_r(App::$hooks,true));
}
/**
* @brief Calls a hook.
*
* Use this function when you want to be able to allow a hook to manipulate
* the provided data.
*
* @param string $name of the hook to call
* @param[in,out] string|array &$data to transmit to the callback handler
*/
static public function call($name, &$data = null)
{
$a = 0;
if (isset(App::$hooks[$name])) {
foreach (App::$hooks[$name] as $hook) {
$origfn = $hook[1];
if ($hook[0]) {
@include_once($hook[0]);
}
if (preg_match('|^a:[0-9]+:{.*}$|s', $hook[1])) {
$hook[1] = unserialize($hook[1]);
} elseif (strpos($hook[1], '::')) {
// We shouldn't need to do this, but it appears that PHP
// isn't able to directly execute a string variable with a class
// method in the manner we are attempting it, so we'll
// turn it into an array.
$hook[1] = explode('::', $hook[1]);
}
if (is_callable($hook[1])) {
$func = $hook[1];
if ($hook[3]) {
$func($data);
} else {
$func($a, $data);
}
} else {
// Don't do any DB write calls if we're currently logging a possibly failed DB call.
if (! DBA::$logging) {
// The hook should be removed so we don't process it.
q(
"DELETE FROM hook WHERE hook = '%s' AND file = '%s' AND fn = '%s'",
dbesc($name),
dbesc($hook[0]),
dbesc($origfn)
);
}
}
}
}
}
}

52
Code/Extend/Route.php Normal file
View file

@ -0,0 +1,52 @@
<?php
namespace Code\Extend;
class Route
{
public static function register($file, $modname)
{
$rt = self::get();
$rt[] = [$file, $modname];
self::set($rt);
}
public static function unregister($file, $modname)
{
$rt = self::get();
if ($rt) {
$n = [];
foreach ($rt as $r) {
if ($r[0] !== $file && $r[1] !== $modname) {
$n[] = $r;
}
}
self::set($n);
}
}
public static function unregister_by_file($file)
{
$rt = self::get();
if ($rt) {
$n = [];
foreach ($rt as $r) {
if ($r[0] !== $file) {
$n[] = $r;
}
}
self::set($n);
}
}
public static function get()
{
return get_config('system', 'routes', []);
}
public static function set($r)
{
return set_config('system', 'routes', $r);
}
}

52
Code/Extend/Widget.php Normal file
View file

@ -0,0 +1,52 @@
<?php
namespace Code\Extend;
class Widget
{
public static function register($file, $widget)
{
$rt = self::get();
$rt[] = [$file, $widget];
self::set($rt);
}
public static function unregister($file, $widget)
{
$rt = self::get();
if ($rt) {
$n = [];
foreach ($rt as $r) {
if ($r[0] !== $file && $r[1] !== $widget) {
$n[] = $r;
}
}
self::set($n);
}
}
public static function unregister_by_file($file)
{
$rt = self::get();
if ($rt) {
$n = [];
foreach ($rt as $r) {
if ($r[0] !== $file) {
$n[] = $r;
}
}
self::set($n);
}
}
public static function get()
{
return get_config('system', 'widgets', []);
}
public static function set($r)
{
return set_config('system', 'widgets', $r);
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace Code\Identity;
use Code\Lib\System;
use OAuth2\Server;
use OAuth2\Storage\Memory;
use OAuth2\GrantType\ClientCredentials;
use OAuth2\OpenID\GrantType\AuthorizationCode;
class OAuth2Server extends Server
{
public function __construct(OAuth2Storage $storage, $config = null)
{
if (! is_array($config)) {
$config = [
// 'use_openid_connect' => true,
'issuer' => System::get_site_name(),
// 'use_jwt_access_tokens' => true,
// 'enforce_state' => false
];
}
parent::__construct($storage, $config);
// Add the "Client Credentials" grant type (it is the simplest of the grant types)
$this->addGrantType(new ClientCredentials($storage));
// Add the "Authorization Code" grant type (this is where the oauth magic happens)
// Need to use OpenID\GrantType to return id_token
// (see:https://github.com/bshaffer/oauth2-server-php/issues/443)
$this->addGrantType(new AuthorizationCode($storage));
$keyStorage = new Memory([
'keys' => [
'public_key' => get_config('system', 'pubkey'),
'private_key' => get_config('system', 'prvkey')
]
]);
$this->addStorage($keyStorage, 'public_key');
}
}

View file

@ -0,0 +1,171 @@
<?php
namespace Code\Identity;
use OAuth2\Storage\Pdo;
use Code\Lib\Channel;
class OAuth2Storage extends Pdo
{
/**
* @param string $username
* @param string $password
* @return bool
*/
public function checkUserCredentials($username, $password)
{
if ($user = $this->getUser($username)) {
return $this->checkPassword($user, $password);
}
return false;
}
/**
* @param string $username
* @return array|bool
*/
public function getUserDetails($username)
{
return $this->getUser($username);
}
/**
*
* @param array $user
* @param string $password
* @return bool
*/
protected function checkPassword($user, $password)
{
$x = account_verify_password($user, $password);
return((array_key_exists('channel', $x) && ! empty($x['channel'])) ? true : false);
}
/**
* @param string $username
* @return array|bool
*/
public function getUser($username)
{
$x = Channel::from_id($username);
if (! $x) {
return false;
}
$a = q(
"select * from account where account_id = %d",
intval($x['channel_account_id'])
);
$n = explode(' ', $x['channel_name']);
return( [
'webfinger' => Channel::get_webfinger($x),
'portable_id' => $x['channel_hash'],
'email' => $a[0]['account_email'],
'username' => $x['channel_address'],
'user_id' => $x['channel_id'],
'name' => $x['channel_name'],
'firstName' => ((count($n) > 1) ? $n[1] : $n[0]),
'lastName' => ((count($n) > 2) ? $n[count($n) - 1] : ''),
'picture' => $x['xchan_photo_l']
] );
}
public function scopeExists($scope)
{
// Report that the scope is valid even if it's not.
// We will only return a very small subset no matter what.
// @TODO: Truly validate the scope
// see vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ScopeInterface.php and
// vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Pdo.php
// for more info.
return true;
}
public function getDefaultScope($client_id = null)
{
// Do not REQUIRE a scope
// see vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ScopeInterface.php and
// for more info.
return null;
}
public function getUserClaims($user_id, $claims)
{
// Populate the CLAIMS requested (if any).
// @TODO: create a more reasonable/comprehensive list.
// @TODO: present claims on the AUTHORIZATION screen
$userClaims = [];
$claims = explode(' ', trim($claims));
$validclaims = [ "name", "preferred_username", "webfinger", "portable_id", "email", "picture", "firstName", "lastName" ];
$claimsmap = [
"webfinger" => 'webfinger',
"portable_id" => 'portable_id',
"name" => 'name',
"email" => 'email',
"preferred_username" => 'username',
"picture" => 'picture',
"given_name" => 'firstName',
"family_name" => 'lastName'
];
$userinfo = $this->getUser($user_id);
foreach ($validclaims as $validclaim) {
if (in_array($validclaim, $claims)) {
$claimkey = $claimsmap[$validclaim];
$userClaims[$validclaim] = $userinfo[$claimkey];
} else {
$userClaims[$validclaim] = $validclaim;
}
}
$userClaims["sub"] = $user_id;
return $userClaims;
}
/**
* plaintext passwords are bad! Override this for your application
*
* @param string $username
* @param string $password
* @param string $firstName
* @param string $lastName
* @return bool
*/
public function setUser($username, $password, $firstName = null, $lastName = null)
{
return true;
}
public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null, $client_name = null)
{
// if it exists, update it.
if ($this->getClientDetails($client_id)) {
$stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_secret=:client_secret, redirect_uri=:redirect_uri, grant_types=:grant_types, scope=:scope, user_id=:user_id, client_name=:client_name where client_id=:client_id', $this->config['client_table']));
} else {
$stmt = $this->db->prepare(sprintf('INSERT INTO %s (client_id, client_secret, redirect_uri, grant_types, scope, user_id, client_name) VALUES (:client_id, :client_secret, :redirect_uri, :grant_types, :scope, :user_id, :client_name)', $this->config['client_table']));
}
return $stmt->execute(compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id', 'client_name'));
}
public function checkRestrictedGrantType($client_id, $grant_type)
{
$details = $this->getClientDetails($client_id);
if ($details['grant_types']) {
$grant_types = explode(' ', $details['grant_types']);
return in_array($grant_type, (array) $grant_types);
}
// if grant_types are not defined, then none are restricted
return true;
}
}

368
Code/Import/Friendica.php Normal file
View file

@ -0,0 +1,368 @@
<?php
namespace Code\Import;
use App;
use Code\Lib\Libzot;
use Code\Lib\PConfig;
use Code\Lib\Connect;
use Code\Lib\Channel;
use Code\Lib\ServiceClass;
use Code\Lib\AccessList;
use Code\Access\PermissionLimits;
use Code\Access\PermissionRoles;
use Code\Access\Permissions;
use Code\Daemon\Run;
use Code\Extend\Hook;
class Friendica
{
private $data;
private $settings;
private $default_group = null;
private $groups = null;
private $members = null;
private $contacts = null;
public function __construct($data, $settings)
{
$this->data = $data;
$this->settings = $settings;
$this->extract();
}
public function extract()
{
// channel stuff
$channel = [
'channel_name' => escape_tags($this->data['user']['username']),
'channel_address' => escape_tags($this->data['user']['nickname']),
'channel_guid' => escape_tags($this->data['user']['guid']),
'channel_guid_sig' => Libzot::sign($this->data['user']['guid'], $this->data['user']['prvkey']),
'channel_hash' => Libzot::make_xchan_hash($this->data['user']['guid'], $this->data['user']['pubkey']),
'channel_prvkey' => $this->data['user']['prvkey'],
'channel_pubkey' => $this->data['user']['pubkey'],
'channel_pageflags' => PAGE_NORMAL,
'channel_expire_days' => intval($this->data['user']['expire']),
'channel_timezone' => escape_tags($this->data['user']['timezone']),
'channel_location' => escape_tags($this->data['user']['default-location'])
];
$account_id = $this->settings['account_id'];
$max_identities = ServiceClass::account_fetch($account_id, 'total_identities');
if ($max_identities !== false) {
$r = q(
"select channel_id from channel where channel_account_id = %d and channel_removed = 0 ",
intval($account_id)
);
if ($r && count($r) > $max_identities) {
notice(sprintf(t('Your service plan only allows %d channels.'), $max_identities) . EOL);
return;
}
}
// save channel or die
$channel = import_channel($channel, $this->settings['account_id'], $this->settings['sieze'], $this->settings['newname']);
if (!$channel) {
logger('no channel');
return;
}
// figure out channel permission roles
$permissions_role = 'social';
$pageflags = ((isset($this->data['user']['page-flags'])) ? intval($this->data['user']['page-flags']) : 0);
if ($pageflags === 2) {
$permissions_role = 'forum';
}
if ($pageflags === 5) {
$permissions_role = 'forum_restricted';
}
if ($pageflags === 0 && isset($this->data['user']['allow_gid']) && $this->data['user']['allow_gid']) {
$permissions_role = 'social_restricted';
}
// Friendica folks only have PERMS_AUTHED and "just me"
$post_comments = (($pageflags === 1) ? 0 : PERMS_AUTHED);
PermissionLimits::Set(local_channel(), 'post_comments', $post_comments);
PConfig::Set($channel['channel_id'], 'system', 'permissions_role', $permissions_role);
PConfig::Set($channel['channel_id'], 'system', 'use_browser_location', (string)intval($this->data['user']['allow_location']));
// find the self contact
$self_contact = null;
if (isset($this->data['contact']) && is_array($this->data['contact'])) {
foreach ($this->data['contact'] as $contact) {
if (isset($contact['self']) && intval($contact['self'])) {
$self_contact = $contact;
break;
}
}
}
if (!is_array($self_contact)) {
logger('self contact not found.');
return;
}
// Create a verified hub location pointing to this site.
$r = hubloc_store_lowlevel(
[
'hubloc_guid' => $channel['channel_guid'],
'hubloc_guid_sig' => $channel['channel_guid_sig'],
'hubloc_id_url' => Channel::url($channel),
'hubloc_hash' => $channel['channel_hash'],
'hubloc_addr' => Channel::get_webfinger($channel),
'hubloc_primary' => 1,
'hubloc_url' => z_root(),
'hubloc_url_sig' => Libzot::sign(z_root(), $channel['channel_prvkey']),
'hubloc_site_id' => Libzot::make_xchan_hash(z_root(), get_config('system', 'pubkey')),
'hubloc_host' => App::get_hostname(),
'hubloc_callback' => z_root() . '/zot',
'hubloc_sitekey' => get_config('system', 'pubkey'),
'hubloc_network' => 'nomad',
'hubloc_updated' => datetime_convert()
]
);
if (!$r) {
logger('Unable to store hub location');
}
if ($self_contact['avatar']) {
$p = z_fetch_url($self_contact['avatar'], true);
if ($p['success']) {
$h = explode("\n", $p['header']);
foreach ($h as $l) {
list($k, $v) = array_map("trim", explode(":", trim($l), 2));
$hdrs[strtolower($k)] = $v;
}
if (array_key_exists('content-type', $hdrs)) {
$phototype = $hdrs['content-type'];
} else {
$phototype = 'image/jpeg';
}
import_channel_photo($p['body'], $phototype, $account_id, $channel['channel_id']);
}
}
$newuid = $channel['channel_id'];
$r = xchan_store_lowlevel(
[
'xchan_hash' => $channel['channel_hash'],
'xchan_guid' => $channel['channel_guid'],
'xchan_guid_sig' => $channel['channel_guid_sig'],
'xchan_pubkey' => $channel['channel_pubkey'],
'xchan_photo_mimetype' => (($photo_type) ? $photo_type : 'image/png'),
'xchan_photo_l' => z_root() . "/photo/profile/l/{$newuid}",
'xchan_photo_m' => z_root() . "/photo/profile/m/{$newuid}",
'xchan_photo_s' => z_root() . "/photo/profile/s/{$newuid}",
'xchan_addr' => Channel::get_webfinger($channel),
'xchan_url' => Channel::url($channel),
'xchan_follow' => z_root() . '/follow?f=&url=%s',
'xchan_connurl' => z_root() . '/poco/' . $channel['channel_address'],
'xchan_name' => $channel['channel_name'],
'xchan_network' => 'nomad',
'xchan_updated' => datetime_convert(),
'xchan_photo_date' => datetime_convert(),
'xchan_name_date' => datetime_convert(),
'xchan_system' => 0
]
);
$r = Channel::profile_store_lowlevel(
[
'aid' => intval($channel['channel_account_id']),
'uid' => intval($newuid),
'profile_guid' => new_uuid(),
'profile_name' => t('Default Profile'),
'is_default' => 1,
'publish' => ((isset($this->data['profile']['publish'])) ? $this->data['profile']['publish'] : 1),
'fullname' => $channel['channel_name'],
'photo' => z_root() . "/photo/profile/l/{$newuid}",
'thumb' => z_root() . "/photo/profile/m/{$newuid}",
'homepage' => ((isset($this->data['profile']['homepage'])) ? $this->data['profile']['homepage'] : EMPTY_STR),
]
);
if ($role_permissions) {
$myperms = ((array_key_exists('perms_connect', $role_permissions)) ? $role_permissions['perms_connect'] : []);
} else {
$x = PermissionRoles::role_perms('social');
$myperms = $x['perms_connect'];
}
$r = abook_store_lowlevel(
[
'abook_account' => intval($channel['channel_account_id']),
'abook_channel' => intval($newuid),
'abook_xchan' => $channel['channel_hash'],
'abook_closeness' => 0,
'abook_created' => datetime_convert(),
'abook_updated' => datetime_convert(),
'abook_self' => 1
]
);
$x = Permissions::serialise(Permissions::FilledPerms($myperms));
set_abconfig($newuid, $channel['channel_hash'], 'system', 'my_perms', $x);
if (intval($channel['channel_account_id'])) {
// Save our permissions role so we can perhaps call it up and modify it later.
if ($role_permissions) {
if (array_key_exists('online', $role_permissions)) {
set_pconfig($newuid, 'system', 'hide_presence', 1 - intval($role_permissions['online']));
}
if (array_key_exists('perms_auto', $role_permissions)) {
$autoperms = intval($role_permissions['perms_auto']);
set_pconfig($newuid, 'system', 'autoperms', $autoperms);
}
}
// Create a group with yourself as a member. This allows somebody to use it
// right away as a default group for new contacts.
AccessList::add($newuid, t('Friends'));
AccessList::member_add($newuid, t('Friends'), $ret['channel']['channel_hash']);
// if our role_permissions indicate that we're using a default collection ACL, add it.
if (is_array($role_permissions) && $role_permissions['default_collection']) {
$r = q(
"select hash from pgrp where uid = %d and gname = '%s' limit 1",
intval($newuid),
dbesc(t('Friends'))
);
if ($r) {
q(
"update channel set channel_default_group = '%s', channel_allow_gid = '%s' where channel_id = %d",
dbesc($r[0]['hash']),
dbesc('<' . $r[0]['hash'] . '>'),
intval($newuid)
);
}
}
set_pconfig($channel['channel_id'], 'system', 'photo_path', '%Y/%Y-%m');
set_pconfig($channel['channel_id'], 'system', 'attach_path', '%Y/%Y-%m');
// auto-follow any of the hub's pre-configured channel choices.
// Only do this if it's the first channel for this account;
// otherwise it could get annoying. Don't make this list too big
// or it will impact registration time.
$accts = get_config('system', 'auto_follow');
if (($accts) && (!$total_identities)) {
if (!is_array($accts)) {
$accts = array($accts);
}
foreach ($accts as $acct) {
if (trim($acct)) {
$f = Channel::connect_and_sync($channel, trim($acct));
if ($f['success']) {
$can_view_stream = their_perms_contains($channel['channel_id'], $f['abook']['abook_xchan'], 'view_stream');
// If we can view their stream, pull in some posts
if (($can_view_stream) || ($f['abook']['xchan_network'] === 'rss')) {
Run::Summon(['Onepoll', $f['abook']['abook_id']]);
}
}
}
}
}
Hook::call('create_identity', $newuid);
}
$this->groups = ((isset($this->data['group'])) ? $this->data['group'] : null);
$this->members = ((isset($this->data['group_member'])) ? $this->data['group_member'] : null);
// import contacts
if (isset($this->data['contact']) && is_array($this->data['contact'])) {
foreach ($this->data['contact'] as $contact) {
if (isset($contact['self']) && intval($contact['self'])) {
continue;
}
logger('connecting: ' . $contact['url'], LOGGER_DEBUG);
$result = Connect::connect($channel, (($contact['addr']) ? $contact['addr'] : $contact['url']));
if ($result['success'] && isset($result['abook'])) {
$contact['xchan_hash'] = $result['abook']['abook_xchan'];
$this->contacts[] = $contact;
}
}
}
// import pconfig
// it is unlikely we can make use of these unless we recongise them.
if (isset($this->data['pconfig']) && is_array($this->data['pconfig'])) {
foreach ($this->data['pconfig'] as $pc) {
$entry = [
'cat' => escape_tags(str_replace('.', '__', $pc['cat'])),
'k' => escape_tags(str_replace('.', '__', $pc['k'])),
'v' => ((preg_match('|^a:[0-9]+:{.*}$|s', $pc['v'])) ? serialise(unserialize($pc['v'])) : $pc['v']),
];
PConfig::Set($channel['channel_id'], $entry['cat'], $entry['k'], $entry['v']);
}
}
// The default 'Friends' group is already created and possibly populated.
// So some of the following code is redundant in that regard.
// Mostly this is used to create and populate any other groups.
if ($this->groups) {
foreach ($this->groups as $group) {
if (!intval($group['deleted'])) {
AccessList::add($channel['channel_id'], $group['name'], intval($group['visible']));
if ($this->members) {
foreach ($this->members as $member) {
if (intval($member['gid']) === intval(AccessList::byname($channel['channel_id'], $group['name']))) {
$contact_id = $member['contact-id'];
if ($this->contacts) {
foreach ($this->contacts as $contact) {
if (intval($contact['id']) === intval($contact_id)) {
AccessList::member_add($channel['channel_id'], $group['name'], $contact['xchan_hash']);
break;
}
}
}
}
}
}
}
}
}
change_channel($channel['channel_id']);
notice(t('Import complete.') . EOL);
goaway(z_root() . '/stream');
}
}

29
Code/Lib/AConfig.php Normal file
View file

@ -0,0 +1,29 @@
<?php
namespace Code\Lib;
// account configuration storage is built on top of the under-utilised xconfig
class AConfig
{
public static function Load($account_id)
{
return XConfig::Load('a_' . $account_id);
}
public static function Get($account_id, $family, $key, $default = false)
{
return XConfig::Get('a_' . $account_id, $family, $key, $default);
}
public static function Set($account_id, $family, $key, $value)
{
return XConfig::Set('a_' . $account_id, $family, $key, $value);
}
public static function Delete($account_id, $family, $key)
{
return XConfig::Delete('a_' . $account_id, $family, $key);
}
}

151
Code/Lib/ASCollection.php Normal file
View file

@ -0,0 +1,151 @@
<?php
namespace Code\Lib;
use Code\Lib\ActivityStreams;
use Code\Lib\Activity;
/**
* Class for dealing with fetching ActivityStreams collections (ordered or unordered, normal or paged).
* Construct with either an existing object or url and an optional channel to sign requests.
* $direction is 0 (default) to fetch from the beginning, and 1 to fetch from the end and reverse order the resultant array.
* An optional limit to the number of records returned may also be specified.
* Use $class->get() to return an array of collection members.
*/
class ASCollection
{
private $channel = null;
private $nextpage = null;
private $limit = 0;
private $direction = 0; // 0 = forward, 1 = reverse
private $data = [];
private $history = [];
public function __construct($obj, $channel = null, $direction = 0, $limit = 0)
{
$this->channel = $channel;
$this->direction = $direction;
$this->limit = $limit;
if (is_array($obj)) {
$data = $obj;
}
if (is_string($obj)) {
$data = Activity::fetch($obj, $channel);
$this->history[] = $obj;
}
if (!is_array($data)) {
return;
}
if (!in_array($data['type'], ['Collection', 'OrderedCollection'])) {
return false;
}
if ($this->direction) {
if (array_key_exists('last', $data) && $data['last']) {
$this->nextpage = $data['last'];
}
} else {
if (array_key_exists('first', $data) && $data['first']) {
$this->nextpage = $data['first'];
}
}
if (isset($data['items']) && is_array($data['items'])) {
$this->data = (($this->direction) ? array_reverse($data['items']) : $data['items']);
} elseif (isset($data['orderedItems']) && is_array($data['orderedItems'])) {
$this->data = (($this->direction) ? array_reverse($data['orderedItems']) : $data['orderedItems']);
}
if ($limit) {
if (count($this->data) > $limit) {
$this->data = array_slice($this->data, 0, $limit);
return;
}
}
do {
$x = $this->next();
} while ($x);
}
public function get()
{
return $this->data;
}
public function next()
{
if (!$this->nextpage) {
return false;
}
if (is_array($this->nextpage)) {
$data = $this->nextpage;
}
if (is_string($this->nextpage)) {
if (in_array($this->nextpage, $this->history)) {
// recursion detected
return false;
}
$data = Activity::fetch($this->nextpage, $this->channel);
$this->history[] = $this->nextpage;
}
if (!is_array($data)) {
return false;
}
if (!in_array($data['type'], ['CollectionPage', 'OrderedCollectionPage'])) {
return false;
}
$this->setnext($data);
if (isset($data['items']) && is_array($data['items'])) {
$this->data = array_merge($this->data, (($this->direction) ? array_reverse($data['items']) : $data['items']));
} elseif (isset($data['orderedItems']) && is_array($data['orderedItems'])) {
$this->data = array_merge($this->data, (($this->direction) ? array_reverse($data['orderedItems']) : $data['orderedItems']));
}
if ($limit) {
if (count($this->data) > $limit) {
$this->data = array_slice($this->data, 0, $limit);
$this->nextpage = false;
return true;
}
}
return true;
}
public function setnext($data)
{
if ($this->direction) {
if (array_key_exists('prev', $data) && $data['prev']) {
$this->nextpage = $data['prev'];
} elseif (array_key_exists('first', $data) && $data['first']) {
$this->nextpage = $data['first'];
} else {
$this->nextpage = false;
}
} else {
if (array_key_exists('next', $data) && $data['next']) {
$this->nextpage = $data['next'];
} elseif (array_key_exists('last', $data) && $data['last']) {
$this->nextpage = $data['last'];
} else {
$this->nextpage = false;
}
}
logger('nextpage: ' . $this->nextpage, LOGGER_DEBUG);
}
}

84
Code/Lib/AbConfig.php Normal file
View file

@ -0,0 +1,84 @@
<?php
namespace Code\Lib;
class AbConfig
{
public static function Load($chan, $xhash, $family = '')
{
if ($family) {
$where = sprintf(" and cat = '%s' ", dbesc($family));
}
$r = q(
"select * from abconfig where chan = %d and xchan = '%s' $where",
intval($chan),
dbesc($xhash)
);
return $r;
}
public static function Get($chan, $xhash, $family, $key, $default = false)
{
$r = q(
"select * from abconfig where chan = %d and xchan = '%s' and cat = '%s' and k = '%s' limit 1",
intval($chan),
dbesc($xhash),
dbesc($family),
dbesc($key)
);
if ($r) {
return unserialise($r[0]['v']);
}
return $default;
}
public static function Set($chan, $xhash, $family, $key, $value)
{
$dbvalue = ((is_array($value)) ? serialise($value) : $value);
$dbvalue = ((is_bool($dbvalue)) ? intval($dbvalue) : $dbvalue);
if (self::Get($chan, $xhash, $family, $key) === false) {
$r = q(
"insert into abconfig ( chan, xchan, cat, k, v ) values ( %d, '%s', '%s', '%s', '%s' ) ",
intval($chan),
dbesc($xhash),
dbesc($family),
dbesc($key),
dbesc($dbvalue)
);
} else {
$r = q(
"update abconfig set v = '%s' where chan = %d and xchan = '%s' and cat = '%s' and k = '%s' ",
dbesc($dbvalue),
dbesc($chan),
dbesc($xhash),
dbesc($family),
dbesc($key)
);
}
if ($r) {
return $value;
}
return false;
}
public static function Delete($chan, $xhash, $family, $key)
{
$r = q(
"delete from abconfig where chan = %d and xchan = '%s' and cat = '%s' and k = '%s' ",
intval($chan),
dbesc($xhash),
dbesc($family),
dbesc($key)
);
return $r;
}
}

494
Code/Lib/AccessList.php Normal file
View file

@ -0,0 +1,494 @@
<?php
namespace Code\Lib;
use Code\Lib\Libsync;
use Code\Render\Theme;
class AccessList
{
public static function add($uid, $name, $public = 0)
{
$ret = false;
if ($uid && $name) {
$r = self::byname($uid, $name); // check for dups
if ($r !== false) {
// This could be a problem.
// Let's assume we've just created a list which we once deleted
// all the old members are gone, but the list remains so we don't break any security
// access lists. What we're doing here is reviving the dead list, but old content which
// was restricted to this list may now be seen by the new list members.
$z = q(
"SELECT * FROM pgrp WHERE id = %d LIMIT 1",
intval($r)
);
if (($z) && $z[0]['deleted']) {
q('UPDATE pgrp SET deleted = 0 WHERE id = %d', intval($z[0]['id']));
notice(t('A deleted list with this name was revived. Existing item permissions <strong>may</strong> apply to this list and any future members. If this is not what you intended, please create another list with a different name.') . EOL);
}
$hash = self::by_id($uid, $r);
return $hash;
}
$hash = new_uuid();
$r = q(
"INSERT INTO pgrp ( hash, uid, visible, gname, rule )
VALUES( '%s', %d, %d, '%s', '' ) ",
dbesc($hash),
intval($uid),
intval($public),
dbesc($name)
);
$ret = $r;
}
Libsync::build_sync_packet($uid, null, true);
return (($ret) ? $hash : $ret);
}
public static function remove($uid, $name)
{
$ret = false;
if ($uid && $name) {
$r = q(
"SELECT id, hash FROM pgrp WHERE uid = %d AND gname = '%s' LIMIT 1",
intval($uid),
dbesc($name)
);
if ($r) {
$group_id = $r[0]['id'];
$group_hash = $r[0]['hash'];
} else {
return false;
}
// remove group from default posting lists
$r = q(
"SELECT channel_default_group, channel_allow_gid, channel_deny_gid FROM channel WHERE channel_id = %d LIMIT 1",
intval($uid)
);
if ($r) {
$user_info = array_shift($r);
$change = false;
if ($user_info['channel_default_group'] == $group_hash) {
$user_info['channel_default_group'] = '';
$change = true;
}
if (strpos($user_info['channel_allow_gid'], '<' . $group_hash . '>') !== false) {
$user_info['channel_allow_gid'] = str_replace('<' . $group_hash . '>', '', $user_info['channel_allow_gid']);
$change = true;
}
if (strpos($user_info['channel_deny_gid'], '<' . $group_hash . '>') !== false) {
$user_info['channel_deny_gid'] = str_replace('<' . $group_hash . '>', '', $user_info['channel_deny_gid']);
$change = true;
}
if ($change) {
q(
"UPDATE channel SET channel_default_group = '%s', channel_allow_gid = '%s', channel_deny_gid = '%s'
WHERE channel_id = %d",
intval($user_info['channel_default_group']),
dbesc($user_info['channel_allow_gid']),
dbesc($user_info['channel_deny_gid']),
intval($uid)
);
}
}
// remove all members
$r = q(
"DELETE FROM pgrp_member WHERE uid = %d AND gid = %d ",
intval($uid),
intval($group_id)
);
// remove group
$r = q(
"UPDATE pgrp SET deleted = 1 WHERE uid = %d AND gname = '%s'",
intval($uid),
dbesc($name)
);
$ret = $r;
}
Libsync::build_sync_packet($uid, null, true);
return $ret;
}
// returns the integer id of an access group owned by $uid and named $name
// or false.
public static function byname($uid, $name)
{
if (!($uid && $name)) {
return false;
}
$r = q(
"SELECT id FROM pgrp WHERE uid = %d AND gname = '%s' LIMIT 1",
intval($uid),
dbesc($name)
);
if ($r) {
return $r[0]['id'];
}
return false;
}
public static function by_id($uid, $id)
{
if (!($uid && $id)) {
return false;
}
$r = q(
"SELECT * FROM pgrp WHERE uid = %d AND id = %d and deleted = 0",
intval($uid),
intval($id)
);
if ($r) {
return array_shift($r);
}
return false;
}
public static function rec_byhash($uid, $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 array_shift($r);
}
return false;
}
public static function member_remove($uid, $name, $member)
{
$gid = self::byname($uid, $name);
if (!$gid) {
return false;
}
if (!($uid && $gid && $member)) {
return false;
}
$r = q(
"DELETE FROM pgrp_member WHERE uid = %d AND gid = %d AND xchan = '%s' ",
intval($uid),
intval($gid),
dbesc($member)
);
Libsync::build_sync_packet($uid, null, true);
return $r;
}
public static function member_add($uid, $name, $member, $gid = 0)
{
if (!$gid) {
$gid = self::byname($uid, $name);
}
if (!($gid && $uid && $member)) {
return false;
}
$r = q(
"SELECT * FROM pgrp_member WHERE uid = %d AND gid = %d AND xchan = '%s' LIMIT 1",
intval($uid),
intval($gid),
dbesc($member)
);
if ($r) {
return true; // You might question this, but
// we indicate success because the group member was in fact created
// -- It was just created at another time
} 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;
}
public static function members($uid, $gid, $total = false, $start = 0, $records = 0)
{
$ret = [];
if ($records) {
$pager_sql = sprintf(" LIMIT %d OFFSET %d ", intval($records), intval($start));
}
// process virtual groups
if (strpos($gid, ':') === 0) {
$vg = substr($gid, 1);
switch ($vg) {
case '1':
$sql_extra = EMPTY_STR;
break;
case '2':
$sql_extra = " and xchan_network in ('nomad','zot6') ";
break;
case '3':
$sql_extra = " and xchan_network = 'activitypub' ";
break;
default:
break;
}
if ($total) {
$r = q(
"SELECT count(*) FROM abook left join xchan on xchan_hash = abook_xchan WHERE abook_channel = %d and xchan_deleted = 0 and abook_self = 0 and abook_blocked = 0 and abook_pending = 0 $sql_extra ORDER BY xchan_name ASC $pager_sql",
intval($uid)
);
return ($r) ? $r[0]['total'] : false;
}
$r = q(
"SELECT * FROM abook left join xchan on xchan_hash = abook_xchan
WHERE abook_channel = %d and xchan_deleted = 0 and abook_self = 0 and abook_blocked = 0 and abook_pending = 0 $sql_extra ORDER BY xchan_name ASC $pager_sql",
intval($uid)
);
if ($r) {
for ($x = 0; $x < count($r); $x++) {
$r[$x]['xchan'] = $r[$x]['abook_xchan'];
}
}
return $r;
}
if (intval($gid)) {
if ($total) {
$r = q(
"SELECT count(xchan) as total 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",
intval($gid),
intval($uid),
intval($uid)
);
if ($r) {
return $r[0]['total'];
}
}
$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 $pager_sql",
intval($gid),
intval($uid),
intval($uid)
);
if ($r) {
$ret = $r;
}
}
return $ret;
}
public static function members_xchan($uid, $gid)
{
$ret = [];
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 $rv) {
$ret[] = $rv['xchan'];
}
}
}
return $ret;
}
public static function select($uid, $group = '')
{
$grps = [];
$r = q(
"SELECT * FROM pgrp WHERE deleted = 0 AND uid = %d ORDER BY gname ASC",
intval($uid)
);
$grps[] = ['name' => '', 'hash' => '0', 'selected' => ''];
if ($r) {
foreach ($r as $rr) {
$grps[] = ['name' => $rr['gname'], 'id' => $rr['hash'], 'selected' => (($group == $rr['hash']) ? 'true' : '')];
}
}
return replace_macros(Theme::get_template('group_selection.tpl'), [
'$label' => t('Add new connections to this access list'),
'$groups' => $grps
]);
}
public static function widget($every = "connections", $each = "lists", $edit = false, $group_id = 0, $cid = '', $mode = 1)
{
$o = '';
$groups = [];
$r = q(
"SELECT * FROM pgrp WHERE deleted = 0 AND uid = %d ORDER BY gname ASC",
intval($_SESSION['uid'])
);
$member_of = [];
if ($cid) {
$member_of = self::containing(local_channel(), $cid);
}
if ($r) {
foreach ($r as $rr) {
$selected = (($group_id == $rr['id']) ? ' group-selected' : '');
if ($edit) {
$groupedit = ['href' => "lists/" . $rr['id'], 'title' => t('edit')];
} else {
$groupedit = null;
}
$groups[] = [
'id' => $rr['id'],
'enc_cid' => base64url_encode($cid),
'cid' => $cid,
'text' => $rr['gname'],
'selected' => $selected,
'href' => (($mode == 0) ? $each . '?f=&gid=' . $rr['id'] : $each . "/" . $rr['id']) . ((x($_GET, 'new')) ? '&new=' . $_GET['new'] : '') . ((x($_GET, 'order')) ? '&order=' . $_GET['order'] : ''),
'edit' => $groupedit,
'ismember' => in_array($rr['id'], $member_of),
];
}
}
return replace_macros(Theme::get_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'),
]);
}
public static function expand($g)
{
if (!(is_array($g) && count($g))) {
return [];
}
$ret = [];
$x = [];
foreach ($g as $gv) {
// virtual access lists
// connections:abc is all the connection sof the channel with channel_hash abc
// zot:abc is all of abc's zot6 connections
// activitypub:abc is all of abc's activitypub connections
if (strpos($gv, 'connections:') === 0 || strpos($gv, 'zot:') === 0 || strpos($gv, 'activitypub:') === 0) {
$sql_extra = EMPTY_STR;
$channel_hash = substr($gv, strpos($gv, ':') + 1);
if (strpos($gv, 'zot:') === 0) {
$sql_extra = " and xchan_network in ('nomad','zot6') ";
}
if (strpos($gv, 'activitypub:') === 0) {
$sql_extra = " and xchan_network = 'activitypub' ";
}
$r = q(
"select channel_id from channel where channel_hash = '%s' ",
dbesc($channel_hash)
);
if ($r) {
foreach ($r as $rv) {
$y = q(
"select abook_xchan from abook left join xchan on abook_xchan = xchan_hash where abook_channel = %d and abook_self = 0 and abook_pending = 0 and abook_archived = 0 $sql_extra",
intval($rv['channel_id'])
);
if ($y) {
foreach ($y as $yv) {
$ret[] = $yv['abook_xchan'];
}
}
}
}
} else {
$x[] = $gv;
}
}
if ($x) {
stringify_array_elms($x, true);
$groups = implode(',', $x);
if ($groups) {
$r = q("SELECT xchan FROM pgrp_member WHERE gid IN ( select id from pgrp where hash in ( $groups ))");
if ($r) {
foreach ($r as $rv) {
$ret[] = $rv['xchan'];
}
}
}
}
return $ret;
}
public static function member_of($c)
{
$r = q(
"SELECT pgrp.gname, pgrp.id FROM pgrp LEFT JOIN pgrp_member ON pgrp_member.gid = pgrp.id
WHERE pgrp_member.xchan = '%s' AND pgrp.deleted = 0 ORDER BY pgrp.gname ASC ",
dbesc($c)
);
return $r;
}
public static function containing($uid, $c)
{
$r = q(
"SELECT gid FROM pgrp_member WHERE uid = %d AND pgrp_member.xchan = '%s' ",
intval($uid),
dbesc($c)
);
$ret = [];
if ($r) {
foreach ($r as $rv) {
$ret[] = $rv['gid'];
}
}
return $ret;
}
}

707
Code/Lib/Account.php Normal file
View file

@ -0,0 +1,707 @@
<?php
namespace Code\Lib;
/**
* @file include/account.php
* @brief Some account related functions.
*/
use App;
use Code\Lib\Crypto;
use Code\Lib\System;
use Code\Lib\Channel;
use Code\Extend\Hook;
use Code\Render\Theme;
class Account {
public static function check_email($email)
{
$email = punify($email);
$result = [ 'error' => false, 'message' => '' ];
// Caution: empty email isn't counted as an error in this function.
// Check for empty value separately.
if (! strlen($email)) {
return $result;
}
if (! validate_email($email)) {
$result['message'] .= t('Not a valid email address') . EOL;
} elseif (! allowed_email($email)) {
$result['message'] = t('Your email domain is not among those allowed on this site');
} else {
$r = q(
"select account_email from account where account_email = '%s' limit 1",
dbesc($email)
);
if ($r) {
$result['message'] .= t('Your email address is already registered at this site.');
}
}
if ($result['message']) {
$result['error'] = true;
}
$arr = array('email' => $email, 'result' => $result);
Hook::call('check_account_email', $arr);
return $arr['result'];
}
public static function check_password($password)
{
$result = [ 'error' => false, 'message' => '' ];
// The only validation we perform by default is pure Javascript to
// check minimum length and that both entered passwords match.
// Use hooked functions to perform complexity requirement checks.
$arr = [ 'password' => $password, 'result' => $result ];
Hook::call('check_account_password', $arr);
return $arr['result'];
}
public static function check_invite($invite_code)
{
$result = [ 'error' => false, 'message' => '' ];
$using_invites = get_config('system', 'invitation_only');
if ($using_invites && defined('INVITE_WORKING')) {
if (! $invite_code) {
$result['message'] .= t('An invitation is required.') . EOL;
}
$r = q("select * from register where hash = '%s' limit 1", dbesc($invite_code));
if (! $r) {
$result['message'] .= t('Invitation could not be verified.') . EOL;
}
}
if (strlen($result['message'])) {
$result['error'] = true;
}
$arr = [ 'invite_code' => $invite_code, 'result' => $result ];
Hook::call('check_account_invite', $arr);
return $arr['result'];
}
public static function check_admin($arr)
{
if (is_site_admin()) {
return true;
}
$admin_email = trim(get_config('system', 'admin_email', ''));
if (strlen($admin_email) && $admin_email === trim($arr['email'])) {
return true;
}
return false;
}
public static function account_total()
{
$r = q("select account_id from account where true");
// Distinguish between an empty array and an error
if (is_array($r)) {
return count($r);
}
return false;
}
public static function account_store_lowlevel($arr)
{
$store = [
'account_parent' => ((array_key_exists('account_parent', $arr)) ? $arr['account_parent'] : '0'),
'account_default_channel' => ((array_key_exists('account_default_channel', $arr)) ? $arr['account_default_channel'] : '0'),
'account_salt' => ((array_key_exists('account_salt', $arr)) ? $arr['account_salt'] : ''),
'account_password' => ((array_key_exists('account_password', $arr)) ? $arr['account_password'] : ''),
'account_email' => ((array_key_exists('account_email', $arr)) ? $arr['account_email'] : ''),
'account_external' => ((array_key_exists('account_external', $arr)) ? $arr['account_external'] : ''),
'account_language' => ((array_key_exists('account_language', $arr)) ? $arr['account_language'] : 'en'),
'account_created' => ((array_key_exists('account_created', $arr)) ? $arr['account_created'] : '0001-01-01 00:00:00'),
'account_lastlog' => ((array_key_exists('account_lastlog', $arr)) ? $arr['account_lastlog'] : '0001-01-01 00:00:00'),
'account_flags' => ((array_key_exists('account_flags', $arr)) ? $arr['account_flags'] : '0'),
'account_roles' => ((array_key_exists('account_roles', $arr)) ? $arr['account_roles'] : '0'),
'account_reset' => ((array_key_exists('account_reset', $arr)) ? $arr['account_reset'] : ''),
'account_expires' => ((array_key_exists('account_expires', $arr)) ? $arr['account_expires'] : '0001-01-01 00:00:00'),
'account_expire_notified' => ((array_key_exists('account_expire_notified', $arr)) ? $arr['account_expire_notified'] : '0001-01-01 00:00:00'),
'account_service_class' => ((array_key_exists('account_service_class', $arr)) ? $arr['account_service_class'] : ''),
'account_level' => ((array_key_exists('account_level', $arr)) ? $arr['account_level'] : '0'),
'account_password_changed' => ((array_key_exists('account_password_changed', $arr)) ? $arr['account_password_changed'] : '0001-01-01 00:00:00')
];
return create_table_from_array('account', $store);
}
public static function create($arr)
{
// Required: { email, password }
$result = [ 'success' => false, 'email' => '', 'password' => '', 'message' => '' ];
$invite_code = ((isset($arr['invite_code'])) ? notags(trim($arr['invite_code'])) : '');
$email = ((isset($arr['email'])) ? notags(punify(trim($arr['email']))) : '');
$password = ((isset($arr['password'])) ? trim($arr['password']) : '');
$password2 = ((isset($arr['password2'])) ? trim($arr['password2']) : '');
$parent = ((isset($arr['parent'])) ? intval($arr['parent']) : 0 );
$flags = ((isset($arr['account_flags'])) ? intval($arr['account_flags']) : ACCOUNT_OK);
$roles = ((isset($arr['account_roles'])) ? intval($arr['account_roles']) : 0 );
$expires = ((isset($arr['expires'])) ? intval($arr['expires']) : NULL_DATE);
$default_service_class = get_config('system', 'default_service_class', EMPTY_STR);
if (! ($email && $password)) {
$result['message'] = t('Please enter the required information.');
return $result;
}
// prevent form hackery
if (($roles & ACCOUNT_ROLE_ADMIN) && (! self::check_admin($arr))) {
$roles = $roles - ACCOUNT_ROLE_ADMIN;
}
// allow the admin_email account to be admin, but only if it's the first account.
$c = self::account_total();
if (($c === 0) && (self::check_admin($arr))) {
$roles |= ACCOUNT_ROLE_ADMIN;
}
// Ensure that there is a host keypair.
if ((! get_config('system', 'pubkey')) && (! get_config('system', 'prvkey'))) {
$hostkey = Crypto::new_keypair(4096);
set_config('system', 'pubkey', $hostkey['pubkey']);
set_config('system', 'prvkey', $hostkey['prvkey']);
}
$invite_result = check_account_invite($invite_code);
if ($invite_result['error']) {
$result['message'] = $invite_result['message'];
return $result;
}
$email_result = check_account_email($email);
if ($email_result['error']) {
$result['message'] = $email_result['message'];
return $result;
}
$password_result = check_account_password($password);
if ($password_result['error']) {
$result['message'] = $password_result['message'];
return $result;
}
$salt = random_string(32);
$password_encoded = hash('whirlpool', $salt . $password);
$r = self::account_store_lowlevel(
[
'account_parent' => intval($parent),
'account_salt' => $salt,
'account_password' => $password_encoded,
'account_email' => $email,
'account_language' => get_best_language(),
'account_created' => datetime_convert(),
'account_flags' => intval($flags),
'account_roles' => intval($roles),
'account_expires' => $expires,
'account_service_class' => $default_service_class
]
);
if (! $r) {
logger('create_account: DB INSERT failed.');
$result['message'] = t('Failed to store account information.');
return($result);
}
$r = q(
"select * from account where account_email = '%s' and account_password = '%s' limit 1",
dbesc($email),
dbesc($password_encoded)
);
if ($r && is_array($r) && count($r)) {
$result['account'] = $r[0];
} else {
logger('create_account: could not retrieve newly created account');
}
// Set the parent record to the current record_id if no parent was provided
if (! $parent) {
$r = q(
"update account set account_parent = %d where account_id = %d",
intval($result['account']['account_id']),
intval($result['account']['account_id'])
);
if (! $r) {
logger('create_account: failed to set parent');
}
$result['account']['parent'] = $result['account']['account_id'];
}
$result['success'] = true;
$result['email'] = $email;
$result['password'] = $password;
Hook::call('register_account', $result);
return $result;
}
public static function verify_email_address($arr)
{
if (array_key_exists('resend', $arr)) {
$email = $arr['email'];
$a = q(
"select * from account where account_email = '%s' limit 1",
dbesc($arr['email'])
);
if (! ($a && ($a[0]['account_flags'] & ACCOUNT_UNVERIFIED))) {
return false;
}
$account = array_shift($a);
$v = q(
"select * from register where uid = %d and password = 'verify' limit 1",
intval($account['account_id'])
);
if ($v) {
$hash = $v[0]['hash'];
} else {
return false;
}
} else {
$hash = random_string(24);
$r = q(
"INSERT INTO register ( hash, created, uid, password, lang ) VALUES ( '%s', '%s', %d, '%s', '%s' ) ",
dbesc($hash),
dbesc(datetime_convert()),
intval($arr['account']['account_id']),
dbesc('verify'),
dbesc($arr['account']['account_language'])
);
$account = $arr['account'];
}
push_lang(($account['account_language']) ? $account['account_language'] : 'en');
$email_msg = replace_macros(
Theme::get_email_template('register_verify_member.tpl'),
[
'$sitename' => System::get_site_name(),
'$siteurl' => z_root(),
'$email' => $arr['email'],
'$uid' => $account['account_id'],
'$hash' => $hash,
'$details' => $details
]
);
$res = z_mail(
[
'toEmail' => $arr['email'],
'messageSubject' => sprintf(t('Registration confirmation for %s'), System::get_site_name()),
'textVersion' => $email_msg,
]
);
pop_lang();
if ($res) {
$delivered ++;
} else {
logger('send_reg_approval_email: failed to account_id: ' . $arr['account']['account_id']);
}
return $res;
}
public static function send_reg_approval_email($arr)
{
$r = q(
"select * from account where (account_roles & %d) >= 4096",
intval(ACCOUNT_ROLE_ADMIN)
);
if (! ($r && is_array($r) && count($r))) {
return false;
}
$admins = [];
foreach ($r as $rr) {
if (strlen($rr['account_email'])) {
$admins[] = [ 'email' => $rr['account_email'], 'lang' => $rr['account_lang'] ];
}
}
if (! count($admins)) {
return false;
}
$hash = random_string();
$r = q(
"INSERT INTO register ( hash, created, uid, password, lang ) VALUES ( '%s', '%s', %d, '%s', '%s' ) ",
dbesc($hash),
dbesc(datetime_convert()),
intval($arr['account']['account_id']),
dbesc(''),
dbesc($arr['account']['account_language'])
);
$ip = ((isset($_SERVER['REMOTE_ADDR'])) ? $_SERVER['REMOTE_ADDR'] : EMPTY_STR);
$details = (($ip) ? $ip . ' [' . gethostbyaddr($ip) . ']' : '[unknown or stealth IP]');
$delivered = 0;
foreach ($admins as $admin) {
if (strlen($admin['lang'])) {
push_lang($admin['lang']);
} else {
push_lang('en');
}
$email_msg = replace_macros(Theme::get_email_template('register_verify_eml.tpl'), [
'$sitename' => get_config('system', 'sitename'),
'$siteurl' => z_root(),
'$email' => $arr['email'],
'$uid' => $arr['account']['account_id'],
'$hash' => $hash,
'$details' => $details
]);
$res = z_mail(
[
'toEmail' => $admin['email'],
'messageSubject' => sprintf(t('Registration request at %s'), get_config('system', 'sitename')),
'textVersion' => $email_msg,
]
);
if ($res) {
$delivered ++;
} else {
logger('send_reg_approval_email: failed to ' . $admin['email'] . 'account_id: ' . $arr['account']['account_id']);
}
pop_lang();
}
return ($delivered ? true : false);
}
public static function send_register_success_email($email, $password)
{
$email_msg = replace_macros(Theme::get_email_template('register_open_eml.tpl'), [
'$sitename' => System::get_site_name(),
'$siteurl' => z_root(),
'$email' => $email,
'$password' => t('your registration password'),
]);
$res = z_mail(
[
'toEmail' => $email,
'messageSubject' => sprintf(t('Registration details for %s'), System::get_site_name()),
'textVersion' => $email_msg,
]
);
return ($res ? true : false);
}
/**
* @brief Allows a user registration.
*
* @param string $hash
* @return array|bool
*/
public static function allow($hash)
{
$ret = array('success' => false);
$register = q(
"SELECT * FROM register WHERE hash = '%s' LIMIT 1",
dbesc($hash)
);
if (! $register) {
return $ret;
}
$account = q(
"SELECT * FROM account WHERE account_id = %d LIMIT 1",
intval($register[0]['uid'])
);
if (! $account) {
return $ret;
}
$r = q(
"DELETE FROM register WHERE hash = '%s'",
dbesc($register[0]['hash'])
);
$r = q(
"update account set account_flags = (account_flags & ~%d) where (account_flags & %d) > 0 and account_id = %d",
intval(ACCOUNT_BLOCKED),
intval(ACCOUNT_BLOCKED),
intval($register[0]['uid'])
);
$r = q(
"update account set account_flags = (account_flags & ~%d) where (account_flags & %d) > 0 and account_id = %d",
intval(ACCOUNT_PENDING),
intval(ACCOUNT_PENDING),
intval($register[0]['uid'])
);
push_lang($register[0]['lang']);
$email_tpl = Theme::get_email_template("register_open_eml.tpl");
$email_msg = replace_macros($email_tpl, [
'$sitename' => System::get_site_name(),
'$siteurl' => z_root(),
'$username' => $account[0]['account_email'],
'$email' => $account[0]['account_email'],
'$password' => '',
'$uid' => $account[0]['account_id']
]);
$res = z_mail(
[
'toEmail' => $account[0]['account_email'],
'messageSubject' => sprintf(t('Registration details for %s'), System::get_site_name()),
'textVersion' => $email_msg,
]
);
pop_lang();
if (get_config('system', 'auto_channel_create')) {
Channel::auto_create($register[0]['uid']);
}
if ($res) {
info(t('Account approved.') . EOL);
return true;
}
}
/**
* @brief Denies an account registration.
*
* This does not have to go through user_remove() and save the nickname
* permanently against re-registration, as the person was not yet
* allowed to have friends on this system
*
* @param string $hash
* @return bool
*/
public static function deny($hash)
{
$register = q(
"SELECT * FROM register WHERE hash = '%s' LIMIT 1",
dbesc($hash)
);
if (! $register) {
return false;
}
$account = q(
"SELECT account_id, account_email FROM account WHERE account_id = %d LIMIT 1",
intval($register[0]['uid'])
);
if (! $account) {
return false;
}
$r = q(
"DELETE FROM account WHERE account_id = %d",
intval($register[0]['uid'])
);
$r = q(
"DELETE FROM register WHERE id = %d",
intval($register[0]['id'])
);
notice(sprintf(t('Registration revoked for %s'), $account[0]['account_email']) . EOL);
return true;
}
// called from regver to activate an account from the email verification link
public static function approve($hash)
{
$ret = false;
// Note: when the password in the register table is 'verify', the uid actually contains the account_id
$register = q(
"SELECT * FROM register WHERE hash = '%s' and password = 'verify' LIMIT 1",
dbesc($hash)
);
if (! $register) {
return $ret;
}
$account = q(
"SELECT * FROM account WHERE account_id = %d LIMIT 1",
intval($register[0]['uid'])
);
if (! $account) {
return $ret;
}
$r = q(
"DELETE FROM register WHERE hash = '%s' and password = 'verify'",
dbesc($register[0]['hash'])
);
$r = q(
"update account set account_flags = (account_flags & ~%d) where (account_flags & %d)>0 and account_id = %d",
intval(ACCOUNT_BLOCKED),
intval(ACCOUNT_BLOCKED),
intval($register[0]['uid'])
);
$r = q(
"update account set account_flags = (account_flags & ~%d) where (account_flags & %d)>0 and account_id = %d",
intval(ACCOUNT_PENDING),
intval(ACCOUNT_PENDING),
intval($register[0]['uid'])
);
$r = q(
"update account set account_flags = (account_flags & ~%d) where (account_flags & %d)>0 and account_id = %d",
intval(ACCOUNT_UNVERIFIED),
intval(ACCOUNT_UNVERIFIED),
intval($register[0]['uid'])
);
// get a fresh copy after we've modified it.
$account = q(
"SELECT * FROM account WHERE account_id = %d LIMIT 1",
intval($register[0]['uid'])
);
if (! $account) {
return $ret;
}
if (get_config('system', 'auto_channel_create')) {
Channel::auto_create($register[0]['uid']);
} else {
$_SESSION['login_return_url'] = 'new_channel';
authenticate_success($account[0], null, true, true, false, true);
}
return true;
}
/**
* Included here for completeness, but this is a very dangerous operation.
* It is the caller's responsibility to confirm the requestor's intent and
* authorisation to do this.
*
* @param int $account_id
* @param bool $local (optional) default true
* @param bool $unset_session (optional) default true
* @return bool|array
*/
public static function remove($account_id, $local = true, $unset_session = true)
{
logger('account_remove: ' . $account_id);
// Global removal (all clones) not currently supported
$local = true;
if (! intval($account_id)) {
logger('No account.');
return false;
}
// Don't let anybody nuke the only admin account.
$r = q(
"select account_id from account where (account_roles & %d) > 0",
intval(ACCOUNT_ROLE_ADMIN)
);
if ($r !== false && count($r) == 1 && $r[0]['account_id'] == $account_id) {
logger("Unable to remove the only remaining admin account");
return false;
}
$r = q(
"select * from account where account_id = %d limit 1",
intval($account_id)
);
if (! $r) {
logger('No account with id: ' . $account_id);
return false;
}
$account_email = $r[0]['account_email'];
$x = q(
"select channel_id from channel where channel_account_id = %d",
intval($account_id)
);
if ($x) {
foreach ($x as $xx) {
Channel::channel_remove($xx['channel_id'], $local, false);
}
}
$r = q(
"delete from account where account_id = %d",
intval($account_id)
);
if ($unset_session) {
App::$session->nuke();
notice(sprintf(t('Account \'%s\' deleted'), $account_email) . EOL);
goaway(z_root());
}
return $r;
}
}

4376
Code/Lib/Activity.php Normal file

File diff suppressed because it is too large Load diff

600
Code/Lib/ActivityPub.php Normal file
View file

@ -0,0 +1,600 @@
<?php
namespace Code\Lib;
use Code\Lib\LDSignatures;
use Code\Lib\ActivityStreams;
use Code\Lib\Activity;
use Code\Lib\Queue;
use Code\Lib\Libsync;
use Code\Daemon\Run;
use Code\Lib\IConfig;
use Code\Lib\Channel;
class ActivityPub
{
public static function notifier_process(&$arr)
{
if ($arr['hub']['hubloc_network'] !== 'activitypub') {
return;
}
logger('upstream: ' . intval($arr['upstream']));
// logger('notifier_array: ' . print_r($arr,true), LOGGER_ALL, LOG_INFO);
$purge_all = (($arr['packet_type'] === 'purge' && (!intval($arr['private']))) ? true : false);
$signed_msg = null;
if (array_key_exists('target_item', $arr) && is_array($arr['target_item'])) {
if (intval($arr['target_item']['item_obscured'])) {
logger('Cannot send raw data as an activitypub activity.');
return;
}
$signed_msg = get_iconfig($arr['target_item'], 'activitypub', 'rawmsg');
// If we have an activity already stored with an LD-signature
// which we are sending downstream, use that signed activity as is.
// The channel will then sign the HTTP transaction.
// It is unclear if Mastodon supports the federation delivery model. Initial tests were
// inconclusive and the behaviour varied.
if (($arr['channel']['channel_hash'] !== $arr['target_item']['author_xchan']) && (!$signed_msg)) {
logger('relayed post with no signed message');
return;
}
}
if ($purge_all) {
$ti = [
'id' => Channel::url($arr['channel']) . '?operation=delete',
'actor' => Channel::url($arr['channel']),
'type' => 'Delete',
'object' => Channel::url($arr['channel']),
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
'cc' => []
];
$msg = array_merge(['@context' => [
ACTIVITYSTREAMS_JSONLD_REV,
'https://w3id.org/security/v1',
Activity::ap_schema()
]], $ti);
$msg['signature'] = LDSignatures::sign($msg, $arr['channel']);
logger('ActivityPub_encoded (purge_all): ' . json_encode($msg, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
$jmsg = json_encode($msg, JSON_UNESCAPED_SLASHES);
} else {
$target_item = $arr['target_item'];
if (!$target_item['mid']) {
return;
}
$prv_recips = $arr['env_recips'];
if ($signed_msg) {
$jmsg = $signed_msg;
} else {
// Rewrite outbound mentions so they match the ActivityPub convention, which
// is to pretend that the preferred display name doesn't exist and instead use
// the username or webfinger address when displaying names. This is likely to
// only cause confusion on nomadic networks where there could be any number
// of applicable webfinger addresses for a given identity.
Activity::rewrite_mentions_sub($target_item, 1, $target_item['obj']);
$ti = Activity::encode_activity($target_item, true);
if (!$ti) {
return;
}
// $token = IConfig::get($target_item['id'],'ocap','relay');
// if ($token) {
// if (defined('USE_BEARCAPS')) {
// $ti['id'] = 'bear:?u=' . $ti['id'] . '&t=' . $token;
// }
// else {
// $ti['id'] = $ti['id'] . '?token=' . $token;
// }
// if ($ti['url'] && is_string($ti['url'])) {
// $ti['url'] .= '?token=' . $token;
// }
// }
$msg = array_merge(['@context' => [
ACTIVITYSTREAMS_JSONLD_REV,
'https://w3id.org/security/v1',
Activity::ap_schema()
]], $ti);
$msg['signature'] = LDSignatures::sign($msg, $arr['channel']);
logger('ActivityPub_encoded: ' . json_encode($msg, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
$jmsg = json_encode($msg, JSON_UNESCAPED_SLASHES);
}
}
if ($prv_recips) {
$hashes = [];
// re-explode the recipients, but only for this hub/pod
foreach ($prv_recips as $recip) {
$hashes[] = "'" . $recip . "'";
}
$r = q(
"select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_url = '%s'
and xchan_hash in (" . implode(',', $hashes) . ") and xchan_network = 'activitypub' ",
dbesc($arr['hub']['hubloc_url'])
);
if (!$r) {
logger('activitypub_process_outbound: no recipients');
return;
}
foreach ($r as $contact) {
// is $contact connected with this channel - and if the channel is cloned, also on this hub?
// 2018-10-19 this probably doesn't apply to activitypub anymore, just send the thing.
// They'll reject it if they don't like it.
// $single = deliverable_singleton($arr['channel']['channel_id'],$contact);
if (!$arr['normal_mode']) {
continue;
}
$qi = self::queue_message($jmsg, $arr['channel'], $contact, $target_item['mid']);
if ($qi) {
$arr['queued'][] = $qi;
}
continue;
}
} else {
// public message
// See if we can deliver all of them at once
$x = get_xconfig($arr['hub']['hubloc_hash'], 'activitypub', 'collections');
if ($x && $x['sharedInbox']) {
logger('using publicInbox delivery for ' . $arr['hub']['hubloc_url'], LOGGER_DEBUG);
$contact['hubloc_callback'] = $x['sharedInbox'];
$qi = self::queue_message($jmsg, $arr['channel'], $contact, $target_item['mid']);
if ($qi) {
$arr['queued'][] = $qi;
}
} else {
$r = q(
"select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_url = '%s' and xchan_network = 'activitypub' ",
dbesc($arr['hub']['hubloc_url'])
);
if (!$r) {
logger('activitypub_process_outbound: no recipients');
return;
}
foreach ($r as $contact) {
// $single = deliverable_singleton($arr['channel']['channel_id'],$contact);
$qi = self::queue_message($jmsg, $arr['channel'], $contact, $target_item['mid']);
if ($qi) {
$arr['queued'][] = $qi;
}
}
}
}
return;
}
public static function queue_message($msg, $sender, $recip, $message_id = '')
{
$dest_url = $recip['hubloc_callback'];
logger('URL: ' . $dest_url, LOGGER_DEBUG);
logger('DATA: ' . jindent($msg), LOGGER_DATA);
if (intval(get_config('system', 'activitypub_test')) || intval(get_pconfig($sender['channel_id'], 'system', 'activitypub_test'))) {
logger('test mode - delivery disabled');
return false;
}
$hash = random_string();
logger('queue: ' . $hash . ' ' . $dest_url, LOGGER_DEBUG);
Queue::insert([
'hash' => $hash,
'account_id' => $sender['channel_account_id'],
'channel_id' => $sender['channel_id'],
'driver' => 'activitypub',
'posturl' => $dest_url,
'notify' => '',
'msg' => $msg
]);
if ($message_id && (!get_config('system', 'disable_dreport'))) {
q(
"insert into dreport ( dreport_mid, dreport_site, dreport_recip, dreport_result, dreport_time, dreport_xchan, dreport_queue, dreport_log ) values ( '%s','%s','%s','%s','%s','%s','%s','%s' ) ",
dbesc($message_id),
dbesc($dest_url),
dbesc($dest_url),
dbesc('queued'),
dbesc(datetime_convert()),
dbesc($sender['channel_hash']),
dbesc($hash),
dbesc(EMPTY_STR)
);
}
return $hash;
}
public static function permissions_update(&$x)
{
if ($x['recipient']['xchan_network'] !== 'activitypub') {
return;
}
self::discover($x['recipient']['xchan_hash'], true);
$x['success'] = true;
}
public static function permissions_create(&$x)
{
// send a follow activity to the followee's inbox
if ($x['recipient']['xchan_network'] !== 'activitypub') {
return;
}
$p = Activity::encode_person($x['sender'], false);
if (!$p) {
return;
}
$orig_follow = get_abconfig($x['sender']['channel_id'], $x['recipient']['xchan_hash'], 'activitypub', 'their_follow_id');
$orig_follow_type = get_abconfig($x['sender']['channel_id'], $x['recipient']['xchan_hash'], 'activitypub', 'their_follow_type');
$msg = array_merge(
['@context' => [
ACTIVITYSTREAMS_JSONLD_REV,
'https://w3id.org/security/v1',
Activity::ap_schema()
]],
[
'id' => z_root() . '/follow/' . $x['recipient']['abook_id'] . (($orig_follow) ? '/' . md5($orig_follow) : EMPTY_STR),
'type' => (($orig_follow_type) ? $orig_follow_type : 'Follow'),
'actor' => $p,
'object' => $x['recipient']['xchan_hash'],
'to' => [$x['recipient']['xchan_hash']],
'cc' => []
]
);
// for Group actors, send both a Follow and a Join because some platforms only support one and there's
// no way of discovering/knowing in advance which type they support
$join_msg = null;
if (intval($x['recipient']['xchan_type']) === XCHAN_TYPE_GROUP) {
$join_msg = $msg;
$join_msg['type'] = 'Join';
$join_msg['signature'] = LDSignatures::sign($join_msg, $x['sender']);
$jmsg2 = json_encode($join_msg, JSON_UNESCAPED_SLASHES);
}
$msg['signature'] = LDSignatures::sign($msg, $x['sender']);
$jmsg = json_encode($msg, JSON_UNESCAPED_SLASHES);
$h = q(
"select * from hubloc where hubloc_hash = '%s' limit 1",
dbesc($x['recipient']['xchan_hash'])
);
if ($h) {
$qi = self::queue_message($jmsg, $x['sender'], $h[0]);
if ($qi) {
$x['deliveries'] = $qi;
}
if ($join_msg) {
$qi = self::queue_message($jmsg2, $x['sender'], $h[0]);
if ($qi) {
$x['deliveries'] = $qi;
}
}
}
$x['success'] = true;
}
public static function permissions_accept(&$x)
{
// send an accept activity to the followee's inbox
if ($x['recipient']['xchan_network'] !== 'activitypub') {
return;
}
// we currently are not handling send of reject follow activities; this is permitted by protocol
$accept = get_abconfig($x['recipient']['abook_channel'], $x['recipient']['xchan_hash'], 'activitypub', 'their_follow_id');
$follow_type = get_abconfig($x['recipient']['abook_channel'], $x['recipient']['xchan_hash'], 'activitypub', 'their_follow_type');
if (!$accept) {
return;
}
$p = Activity::encode_person($x['sender'], false);
if (!$p) {
return;
}
$msg = array_merge(
['@context' => [
ACTIVITYSTREAMS_JSONLD_REV,
'https://w3id.org/security/v1',
Activity::ap_schema()
]],
[
'id' => z_root() . '/follow/' . $x['recipient']['abook_id'] . '/' . md5($accept),
'type' => 'Accept',
'actor' => $p,
'object' => [
'type' => (($follow_type) ? $follow_type : 'Follow'),
'id' => $accept,
'actor' => $x['recipient']['xchan_hash'],
'object' => z_root() . '/channel/' . $x['sender']['channel_address']
],
'to' => [$x['recipient']['xchan_hash']],
'cc' => []
]
);
$msg['signature'] = LDSignatures::sign($msg, $x['sender']);
$jmsg = json_encode($msg, JSON_UNESCAPED_SLASHES);
$h = q(
"select * from hubloc where hubloc_hash = '%s' limit 1",
dbesc($x['recipient']['xchan_hash'])
);
if ($h) {
$qi = self::queue_message($jmsg, $x['sender'], $h[0]);
if ($qi) {
$x['deliveries'] = $qi;
}
}
$x['success'] = true;
}
public static function contact_remove($channel_id, $abook)
{
$recip = q(
"select * from abook left join xchan on abook_xchan = xchan_hash where abook_id = %d",
intval($abook['abook_id'])
);
if ((!$recip) || $recip[0]['xchan_network'] !== 'activitypub') {
return;
}
$channel = Channel::from_id($recip[0]['abook_channel']);
if (!$channel) {
return;
}
$p = Activity::encode_person($channel, true, true);
if (!$p) {
return;
}
// send an unfollow activity to the followee's inbox
$orig_activity = get_abconfig($recip[0]['abook_channel'], $recip[0]['xchan_hash'], 'activitypub', 'follow_id');
if ($orig_activity && $recip[0]['abook_pending']) {
// was never approved
$msg = array_merge(
['@context' => [
ACTIVITYSTREAMS_JSONLD_REV,
'https://w3id.org/security/v1',
Activity::ap_schema()
]],
[
'id' => z_root() . '/follow/' . $recip[0]['abook_id'] . '/' . md5($orig_activity) . '?operation=reject',
'type' => 'Reject',
'actor' => $p,
'object' => [
'type' => 'Follow',
'id' => $orig_activity,
'actor' => $recip[0]['xchan_hash'],
'object' => $p
],
'to' => [$recip[0]['xchan_hash']],
'cc' => []
]
);
del_abconfig($recip[0]['abook_channel'], $recip[0]['xchan_hash'], 'activitypub', 'follow_id');
} else {
// send an unfollow
$msg = array_merge(
['@context' => [
ACTIVITYSTREAMS_JSONLD_REV,
'https://w3id.org/security/v1',
Activity::ap_schema()
]],
[
'id' => z_root() . '/follow/' . $recip[0]['abook_id'] . (($orig_activity) ? '/' . md5($orig_activity) : EMPTY_STR) . '?operation=unfollow',
'type' => 'Undo',
'actor' => $p,
'object' => [
'id' => z_root() . '/follow/' . $recip[0]['abook_id'] . (($orig_activity) ? '/' . md5($orig_activity) : EMPTY_STR),
'type' => 'Follow',
'actor' => $p,
'object' => $recip[0]['xchan_hash']
],
'to' => [$recip[0]['xchan_hash']],
'cc' => []
]
);
}
$msg['signature'] = LDSignatures::sign($msg, $channel);
$jmsg = json_encode($msg, JSON_UNESCAPED_SLASHES);
$h = q(
"select * from hubloc where hubloc_hash = '%s' limit 1",
dbesc($recip[0]['xchan_hash'])
);
if ($h) {
$qi = self::queue_message($jmsg, $channel, $h[0]);
if ($qi) {
Run::Summon(['Deliver', $qi]);
}
}
}
public static function discover($apurl, $force = false)
{
$person_obj = null;
$ap = Activity::fetch($apurl);
if ($ap) {
$AS = new ActivityStreams($ap);
if ($AS->is_valid()) {
if (ActivityStreams::is_an_actor($AS->type)) {
$person_obj = $AS->data;
} elseif ($AS->obj && ActivityStreams::is_an_actor($AS->obj['type'])) {
$person_obj = $AS->obj;
}
}
}
if (isset($person_obj)) {
Activity::actor_store($person_obj['id'], $person_obj, $force);
return $person_obj['id'];
}
return false;
}
public static function move($src, $dst)
{
if (!($src && $dst)) {
return;
}
if ($src && !is_array($src)) {
$src = Activity::fetch($src);
if (is_array($src)) {
$src_xchan = $src['id'];
}
}
$approvals = null;
if ($dst && !is_array($dst)) {
$dst = Activity::fetch($dst);
if (is_array($dst)) {
$dst_xchan = $dst['id'];
if (array_key_exists('alsoKnownAs', $dst)) {
if (!is_array($dst['alsoKnownAs'])) {
$dst['alsoKnownAs'] = [$dst['alsoKnownAs']];
}
$approvals = $dst['alsoKnownAs'];
}
}
}
if (!($src_xchan && $dst_xchan)) {
return;
}
if ($approvals) {
foreach ($approvals as $approval) {
if ($approval === $src_xchan) {
$abooks = q(
"select abook_channel from abook where abook_xchan = '%s'",
dbesc($src_xchan)
);
if ($abooks) {
foreach ($abooks as $abook) {
// check to see if we already performed this action
$x = q(
"select * from abook where abook_xchan = '%s' and abook_channel = %d",
dbesc($dst_xchan),
intval($abook['abook_channel'])
);
if ($x) {
continue;
}
// update the local abook
q(
"update abconfig set xchan = '%s' where chan = %d and xchan = '%s'",
dbesc($dst_xchan),
intval($abook['abook_channel']),
dbesc($src_xchan)
);
q(
"update pgrp_member set xchan = '%s' where uid = %d and xchan = '%s'",
dbesc($dst_xchan),
intval($abook['abook_channel']),
dbesc($src_xchan)
);
$r = q(
"update abook set abook_xchan = '%s' where abook_xchan = '%s' and abook_channel = %d ",
dbesc($dst_xchan),
dbesc($src_xchan),
intval($abook['abook_channel'])
);
$r = q(
"SELECT abook.*, xchan.*
FROM abook left join xchan on abook_xchan = xchan_hash
WHERE abook_channel = %d and abook_id = %d LIMIT 1",
intval(abook['abook_channel']),
intval($dst_xchan)
);
if ($r) {
$clone = array_shift($r);
unset($clone['abook_id']);
unset($clone['abook_account']);
unset($clone['abook_channel']);
$abconfig = load_abconfig($abook['abook_channel'], $clone['abook_xchan']);
if ($abconfig) {
$clone['abconfig'] = $abconfig;
}
Libsync::build_sync_packet($abook['abook_channel'], ['abook' => [$clone]]);
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,502 @@
<?php
namespace Code\Lib;
use Code\Web\HTTPSig;
/**
* @brief ActivityStreams class.
*
* Parses an ActivityStream JSON string.
*/
class ActivityStreams
{
public $raw = null;
public $data = null;
public $meta = null;
public $hub = null;
public $valid = false;
public $deleted = false;
public $id = '';
public $parent_id = '';
public $type = '';
public $actor = null;
public $obj = null;
public $tgt = null;
public $replyto = null;
public $origin = null;
public $owner = null;
public $signer = null;
public $ldsig = null;
public $sigok = false;
public $recips = null;
public $raw_recips = null;
public $implied_create = false;
/**
* @brief Constructor for ActivityStreams.
*
* Takes a JSON string or previously decode activity array as parameter,
* decodes it and sets up this object/activity, fetching any required attributes
* which were only referenced by @id/URI.
*
* @param string $string
*/
public function __construct($string, $hub = null, $client = null)
{
$this->raw = $string;
$this->hub = $hub;
if (is_array($string)) {
$this->data = $string;
$this->raw = json_encode($string, JSON_UNESCAPED_SLASHES);
} else {
$this->data = json_decode($string, true);
}
if ($this->data) {
// verify and unpack JSalmon signature if present
// This will only be the case for Zot6 packets
if (is_array($this->data) && array_key_exists('signed', $this->data)) {
$ret = JSalmon::verify($this->data);
$tmp = JSalmon::unpack($this->data['data']);
if ($ret && $ret['success']) {
if ($ret['signer']) {
logger('Unpacked: ' . json_encode($tmp, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOGGER_DATA, LOG_DEBUG);
$saved = json_encode($this->data, JSON_UNESCAPED_SLASHES);
$this->data = $tmp;
$this->meta['signer'] = $ret['signer'];
$this->meta['signed_data'] = $saved;
if ($ret['hubloc']) {
$this->meta['hubloc'] = $ret['hubloc'];
}
}
}
}
// This indicates only that we have sucessfully decoded JSON.
$this->valid = true;
// Special handling for Mastodon "delete actor" activities which will often fail to verify
// because the key cannot be fetched. We will catch this condition elsewhere.
if (array_key_exists('type', $this->data) && array_key_exists('actor', $this->data) && array_key_exists('object', $this->data)) {
if ($this->data['type'] === 'Delete' && $this->data['actor'] === $this->data['object']) {
$this->deleted = $this->data['actor'];
$this->valid = false;
}
}
}
// Attempt to assemble an Activity from what we were given.
if ($this->is_valid()) {
$this->id = $this->get_property_obj('id');
$this->type = $this->get_primary_type();
$this->actor = $this->get_actor('actor', '', '');
$this->obj = $this->get_compound_property('object');
$this->tgt = $this->get_compound_property('target');
$this->origin = $this->get_compound_property('origin');
$this->recips = $this->collect_recips();
$this->replyto = $this->get_property_obj('replyTo');
$this->ldsig = $this->get_compound_property('signature');
if ($this->ldsig) {
$this->signer = $this->get_compound_property('creator', $this->ldsig);
if (
$this->signer && is_array($this->signer) && array_key_exists('publicKey', $this->signer)
&& is_array($this->signer['publicKey']) && $this->signer['publicKey']['publicKeyPem']
) {
$this->sigok = LDSignatures::verify($this->data, $this->signer['publicKey']['publicKeyPem']);
}
}
// Implied create activity required by C2S specification if no object is present
if (!$this->obj) {
if (!$client) {
$this->implied_create = true;
}
$this->obj = $this->data;
$this->type = 'Create';
if (!$this->actor) {
$this->actor = $this->get_actor('attributedTo', $this->obj);
}
}
// fetch recursive or embedded activities
if ($this->obj && is_array($this->obj) && array_key_exists('object', $this->obj)) {
$this->obj['object'] = $this->get_compound_property($this->obj['object']);
}
// Enumerate and store actors in referenced objects
if ($this->obj && is_array($this->obj) && isset($this->obj['actor'])) {
$this->obj['actor'] = $this->get_actor('actor', $this->obj);
}
if ($this->tgt && is_array($this->tgt) && isset($this->tgt['actor'])) {
$this->tgt['actor'] = $this->get_actor('actor', $this->tgt);
}
// Determine if this is a followup or response activity
$this->parent_id = $this->get_property_obj('inReplyTo');
if ((!$this->parent_id) && is_array($this->obj)) {
$this->parent_id = $this->obj['inReplyTo'];
}
if ((!$this->parent_id) && is_array($this->obj)) {
$this->parent_id = $this->obj['id'];
}
}
}
/**
* @brief Return if instantiated ActivityStream is valid.
*
* @return bool Return true if the JSON string could be decoded.
*/
public function is_valid()
{
return $this->valid;
}
public function set_recips($arr)
{
$this->saved_recips = $arr;
}
/**
* @brief Collects all recipients.
*
* @param string $base
* @param string $namespace (optional) default empty
* @return array
*/
public function collect_recips($base = '', $namespace = '')
{
$x = [];
$fields = ['to', 'cc', 'bto', 'bcc', 'audience'];
foreach ($fields as $f) {
// don't expand these yet
$y = $this->get_property_obj($f, $base, $namespace);
if ($y) {
if (!is_array($this->raw_recips)) {
$this->raw_recips = [];
}
if (!is_array($y)) {
$y = [$y];
}
$this->raw_recips[$f] = $y;
$x = array_merge($x, $y);
}
}
// not yet ready for prime time
// $x = $this->expand($x,$base,$namespace);
return $x;
}
public function expand($arr, $base = '', $namespace = '')
{
$ret = [];
// right now use a hardwired recursion depth of 5
for ($z = 0; $z < 5; $z++) {
if (is_array($arr) && $arr) {
foreach ($arr as $a) {
if (is_array($a)) {
$ret[] = $a;
} else {
$x = $this->get_compound_property($a, $base, $namespace);
if ($x) {
$ret = array_merge($ret, $x);
}
}
}
}
}
/// @fixme de-duplicate
return $ret;
}
/**
* @brief
*
* @param array $base
* @param string $namespace if not set return empty string
* @return string|NULL
*/
public function get_namespace($base, $namespace)
{
if (!$namespace) {
return EMPTY_STR;
}
$key = null;
foreach ([$this->data, $base] as $b) {
if (!$b) {
continue;
}
if (array_key_exists('@context', $b)) {
if (is_array($b['@context'])) {
foreach ($b['@context'] as $ns) {
if (is_array($ns)) {
foreach ($ns as $k => $v) {
if ($namespace === $v) {
$key = $k;
}
}
} else {
if ($namespace === $ns) {
$key = '';
}
}
}
} else {
if ($namespace === $b['@context']) {
$key = '';
}
}
}
}
return $key;
}
/**
* @brief
*
* @param string $property
* @param array $base (optional)
* @param string $namespace (optional) default empty
* @return NULL|mixed
*/
public function get_property_obj($property, $base = '', $namespace = '')
{
$prefix = $this->get_namespace($base, $namespace);
if ($prefix === null) {
return null;
}
$base = (($base) ? $base : $this->data);
$propname = (($prefix) ? $prefix . ':' : '') . $property;
if (!is_array($base)) {
btlogger('not an array: ' . print_r($base, true));
return null;
}
return ((array_key_exists($propname, $base)) ? $base[$propname] : null);
}
/**
* @brief Fetches a property from an URL.
*
* @param string $url
* @param array $channel (signing channel, default system channel)
* @return NULL|mixed
*/
public function fetch_property($url, $channel = null, $hub = null)
{
$x = Activity::fetch($url, $channel, $hub);
if ($x === null && strpos($url, '/channel/')) {
// look for other nomadic channels which might be alive
$zf = Zotfinger::exec($url, $channel);
$url = $zf['signature']['signer'];
$x = Activity::fetch($url, $channel);
}
return $x;
}
/**
* @brief given a type, determine if this object represents an actor
*
* If $type is an array, recurse through each element and return true if any
* of the elements are a known actor type
*
* @param string|array $type
* @return boolean
*/
public static function is_an_actor($type)
{
if (!$type) {
return false;
}
if (is_array($type)) {
foreach ($type as $x) {
if (self::is_an_actor($x)) {
return true;
}
}
return false;
}
return (in_array($type, ['Application', 'Group', 'Organization', 'Person', 'Service']));
}
public static function is_response_activity($s)
{
if (!$s) {
return false;
}
return (in_array($s, ['Like', 'Dislike', 'Flag', 'Block', 'Announce', 'Accept', 'Reject', 'TentativeAccept', 'TentativeReject', 'emojiReaction', 'EmojiReaction', 'EmojiReact']));
}
/**
* @brief
*
* @param string $property
* @param array $base
* @param string $namespace (optional) default empty
* @return NULL|mixed
*/
public function get_actor($property, $base = '', $namespace = '')
{
$x = $this->get_property_obj($property, $base, $namespace);
if (self::is_url($x)) {
$y = Activity::get_cached_actor($x);
if ($y) {
return $y;
}
}
$actor = $this->get_compound_property($property, $base, $namespace, true);
if (is_array($actor) && self::is_an_actor($actor['type'])) {
if (array_key_exists('id', $actor) && (!array_key_exists('inbox', $actor))) {
$actor = $this->fetch_property($actor['id']);
}
return $actor;
}
return null;
}
/**
* @brief
*
* @param string $property
* @param array $base
* @param string $namespace (optional) default empty
* @param bool $first (optional) default false, if true and result is a sequential array return only the first element
* @return NULL|mixed
*/
public function get_compound_property($property, $base = '', $namespace = '', $first = false)
{
$x = $this->get_property_obj($property, $base, $namespace);
if (self::is_url($x)) {
$y = $this->fetch_property($x);
if (is_array($y)) {
$x = $y;
}
}
// verify and unpack JSalmon signature if present
// This may be present in Zot6 packets
if (is_array($x) && array_key_exists('signed', $x)) {
$ret = JSalmon::verify($x);
$tmp = JSalmon::unpack($x['data']);
if ($ret && $ret['success']) {
if ($ret['signer']) {
logger('Unpacked: ' . json_encode($tmp, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOGGER_DATA, LOG_DEBUG);
$saved = json_encode($x, JSON_UNESCAPED_SLASHES);
$x = $tmp;
$x['meta']['signer'] = $ret['signer'];
$x['meta']['signed_data'] = $saved;
if ($ret['hubloc']) {
$x['meta']['hubloc'] = $ret['hubloc'];
}
}
}
}
if ($first && is_array($x) && array_key_exists(0, $x)) {
return $x[0];
}
return $x;
}
/**
* @brief Check if string starts with http.
*
* @param string $url
* @return bool
*/
public static function is_url($url)
{
if (($url) && (!is_array($url)) && ((strpos($url, 'http') === 0) || (strpos($url, 'x-zot') === 0) || (strpos($url, 'bear') === 0))) {
return true;
}
return false;
}
/**
* @brief Gets the type property.
*
* @param array $base
* @param string $namespace (optional) default empty
* @return NULL|mixed
*/
public function get_primary_type($base = '', $namespace = '')
{
if (!$base) {
$base = $this->data;
}
$x = $this->get_property_obj('type', $base, $namespace);
if (is_array($x)) {
foreach ($x as $y) {
if (strpos($y, ':') === false) {
return $y;
}
}
}
return $x;
}
public function debug()
{
$x = var_export($this, true);
return $x;
}
public static function is_as_request()
{
$x = getBestSupportedMimeType([
'application/ld+json;profile="https://www.w3.org/ns/activitystreams"',
'application/activity+json',
'application/ld+json;profile="http://www.w3.org/ns/activitystreams"',
'application/ld+json', // required for Friendica ~2021-09, can possibly be removed after next release of that project
'application/x-zot-activity+json'
]);
return (($x) ? true : false);
}
}

370
Code/Lib/Addon.php Normal file
View file

@ -0,0 +1,370 @@
<?php
namespace Code\Lib;
use App;
use Code\Lib\Infocon;
class Addon {
/**
* @brief Handle errors in plugin calls
*
* @param string $addon name of the addon
* @param string $error_text text of error
* @param bool $uninstall uninstall plugin
*/
public static function ErrorHandler($addon, $notice, $log, $uninstall = false)
{
logger("Addons: [" . $addon . "] Error: ".$log, LOGGER_ERROR);
if ($notice != '') {
notice("[" . $addon . "] Error: ".$notice, LOGGER_ERROR);
}
if ($uninstall) {
self::uninstall($addon);
}
}
/**
* @brief Unloads an addon.
*
* @param string $addon name of the addon
*/
public static function unload($addon)
{
logger("Addons: unloading " . $addon, LOGGER_DEBUG);
@include_once('addon/' . $addon . '/' . $addon . '.php');
if (function_exists($addon . '_unload')) {
$func = $addon . '_unload';
try {
$func();
} catch (Exception $e) {
self::ErrorHandler($addon, "Unable to unload.", $e->getMessage());
}
}
}
/**
* @brief Uninstalls an addon.
*
* @param string $addon name of the addon
* @return bool
*/
public static function uninstall($addon)
{
self::unload($addon);
if (! file_exists('addon/' . $addon . '/' . $addon . '.php')) {
q(
"DELETE FROM addon WHERE aname = '%s' ",
dbesc($addon)
);
return false;
}
logger("Addons: uninstalling " . $addon);
//$t = @filemtime('addon/' . $addon . '/' . $addon . '.php');
@include_once('addon/' . $addon . '/' . $addon . '.php');
if (function_exists($addon . '_uninstall')) {
$func = $addon . '_uninstall';
try {
$func();
} catch (Exception $e) {
self::ErrorHandler($addon, "Unable to uninstall.", "Unable to run _uninstall : ".$e->getMessage());
}
}
q(
"DELETE FROM addon WHERE aname = '%s' ",
dbesc($addon)
);
}
/**
* @brief Installs an addon.
*
* This function is called once to install the addon (either from the cli or via
* the web admin). This will also call load_plugin() once.
*
* @param string $addon name of the addon
* @return bool
*/
public static function install($addon)
{
if (! file_exists('addon/' . $addon . '/' . $addon . '.php')) {
return false;
}
logger("Addons: installing " . $addon);
$t = @filemtime('addon/' . $addon . '/' . $addon . '.php');
@include_once('addon/' . $addon . '/' . $addon . '.php');
if (function_exists($addon . '_install')) {
$func = $addon . '_install';
try {
$func();
} catch (Exception $e) {
self::ErrorHandler($addon, "Install failed.", "Install failed : ".$e->getMessage());
return;
}
}
$addon_admin = (function_exists($addon . '_plugin_admin') ? 1 : 0);
$d = q(
"select * from addon where aname = '%s' limit 1",
dbesc($addon)
);
if (! $d) {
q(
"INSERT INTO addon (aname, installed, tstamp, plugin_admin) VALUES ( '%s', 1, %d , %d ) ",
dbesc($addon),
intval($t),
$addon_admin
);
}
self::load($addon);
}
/**
* @brief loads an addon by it's name.
*
* @param string $addon name of the addon
* @return bool
*/
public static function load($addon)
{
// silently fail if plugin was removed
if (! file_exists('addon/' . $addon . '/' . $addon . '.php')) {
return false;
}
logger("Addons: loading " . $addon, LOGGER_DEBUG);
//$t = @filemtime('addon/' . $addon . '/' . $addon . '.php');
@include_once('addon/' . $addon . '/' . $addon . '.php');
if (function_exists($addon . '_load')) {
$func = $addon . '_load';
try {
$func();
} catch (Exception $e) {
self::ErrorHandler($addon, "Unable to load.", "FAILED loading : ".$e->getMessage(), true);
return;
}
// we can add the following with the previous SQL
// once most site tables have been updated.
// This way the system won't fall over dead during the update.
if (file_exists('addon/' . $addon . '/.hidden')) {
q(
"update addon set hidden = 1 where name = '%s'",
dbesc($addon)
);
}
return true;
} else {
logger("Addons: FAILED loading " . $addon . " (missing _load function)");
return false;
}
}
/**
* @brief Check if addon is installed.
*
* @param string $name
* @return bool
*/
public static function is_installed($name)
{
$r = q(
"select aname from addon where aname = '%s' and installed = 1 limit 1",
dbesc($name)
);
if ($r) {
return true;
}
return false;
}
/**
* @brief Reload all updated plugins.
*/
public static function reload_all()
{
$addons = get_config('system', 'addon');
if (strlen($addons)) {
$r = q("SELECT * FROM addon WHERE installed = 1");
if (count($r)) {
$installed = $r;
} else {
$installed = [];
}
$parr = explode(',', $addons);
if (count($parr)) {
foreach ($parr as $pl) {
$pl = trim($pl);
$fname = 'addon/' . $pl . '/' . $pl . '.php';
if (file_exists($fname)) {
$t = @filemtime($fname);
foreach ($installed as $i) {
if (($i['aname'] == $pl) && ($i['tstamp'] != $t)) {
logger('Reloading plugin: ' . $i['aname']);
@include_once($fname);
if (function_exists($pl . '_unload')) {
$func = $pl . '_unload';
try {
$func();
} catch (Exception $e) {
self::ErrorHandler($addon, "", "UNLOAD FAILED (uninstalling) : ".$e->getMessage(), true);
continue;
}
}
if (function_exists($pl . '_load')) {
$func = $pl . '_load';
try {
$func();
} catch (Exception $e) {
self::ErrorHandler($addon, "", "LOAD FAILED (uninstalling): ".$e->getMessage(), true);
continue;
}
}
q(
"UPDATE addon SET tstamp = %d WHERE id = %d",
intval($t),
intval($i['id'])
);
}
}
}
}
}
}
}
public static function list_installed()
{
$r = q("select * from addon where installed = 1 order by aname asc");
return(($r) ? ids_to_array($r, 'aname') : []);
}
/**
* @brief Get a list of non hidden addons.
*
* @return array
*/
public static function list_visible()
{
$r = q("select * from addon where hidden = 0 order by aname asc");
$x = (($r) ? ids_to_array($r, 'aname') : []);
$y = [];
if ($x) {
foreach ($x as $xv) {
if (is_dir('addon/' . $xv)) {
$y[] = $xv;
}
}
}
return $y;
}
/**
* @brief Parse plugin comment in search of plugin infos.
*
* like
* \code
* * Name: Plugin
* * Description: A plugin which plugs in
* * Version: 1.2.3
* * Author: John <profile url>
* * Author: Jane <email>
* *
*\endcode
* @param string $plugin the name of the plugin
* @return array with the plugin information
*/
public static function get_info($plugin)
{
$info = null;
if (is_file("addon/$plugin/$plugin.yml")) {
$info = Infocon::from_file("addon/$plugin/$plugin.yml");
}
elseif (is_file("addon/$plugin/$plugin.php")) {
$info = Infocon::from_c_comment("addon/$plugin/$plugin.php");
}
return $info ? $info : [ 'name' => $plugin ] ;
}
public static function check_versions($info)
{
if (! is_array($info)) {
return true;
}
if (array_key_exists('minversion', $info) && $info['minversion']) {
if (! version_compare(STD_VERSION, trim($info['minversion']), '>=')) {
logger('minversion limit: ' . $info['name'], LOGGER_NORMAL, LOG_WARNING);
return false;
}
}
if (array_key_exists('maxversion', $info) && $info['maxversion']) {
if (! version_compare(STD_VERSION, trim($info['maxversion']), '<')) {
logger('maxversion limit: ' . $info['name'], LOGGER_NORMAL, LOG_WARNING);
return false;
}
}
if (array_key_exists('minphpversion', $info) && $info['minphpversion']) {
if (! version_compare(PHP_VERSION, trim($info['minphpversion']), '>=')) {
logger('minphpversion limit: ' . $info['name'], LOGGER_NORMAL, LOG_WARNING);
return false;
}
}
if (array_key_exists('requires', $info)) {
$arr = explode(',', $info['requires']);
$found = true;
if ($arr) {
foreach ($arr as $test) {
$test = trim($test);
if (! $test) {
continue;
}
if (strpos($test, '.')) {
$conf = explode('.', $test);
if (get_config(trim($conf[0]), trim($conf[1]))) {
return true;
} else {
return false;
}
}
if (! in_array($test, Addon::list_installed())) {
$found = false;
}
}
}
if (! $found) {
return false;
}
}
return true;
}
}

34
Code/Lib/Api_router.php Normal file
View file

@ -0,0 +1,34 @@
<?php
namespace Code\Lib;
class Api_router
{
private static $routes = [];
public static function register($path, $fn, $auth_required)
{
self::$routes[$path] = ['func' => $fn, 'auth' => $auth_required];
}
public static function find($path)
{
if (array_key_exists($path, self::$routes)) {
return self::$routes[$path];
}
$with_params = dirname($path) . '/[id]';
if (array_key_exists($with_params, self::$routes)) {
return self::$routes[$with_params];
}
return null;
}
public static function dbg()
{
return self::$routes;
}
}

1404
Code/Lib/Apps.php Normal file

File diff suppressed because it is too large Load diff

63
Code/Lib/Cache.php Normal file
View file

@ -0,0 +1,63 @@
<?php
/** @file */
namespace Code\Lib;
/**
* cache api
*/
class Cache
{
public static function get($key)
{
$hash = hash('whirlpool', $key);
$r = q(
"SELECT v FROM cache WHERE k = '%s' limit 1",
dbesc($hash)
);
if ($r) {
return $r[0]['v'];
}
return null;
}
public static function set($key, $value)
{
$hash = hash('whirlpool', $key);
$r = q(
"SELECT * FROM cache WHERE k = '%s' limit 1",
dbesc($hash)
);
if ($r) {
q(
"UPDATE cache SET v = '%s', updated = '%s' WHERE k = '%s'",
dbesc($value),
dbesc(datetime_convert()),
dbesc($hash)
);
} else {
q(
"INSERT INTO cache ( k, v, updated) VALUES ('%s','%s','%s')",
dbesc($hash),
dbesc($value),
dbesc(datetime_convert())
);
}
}
public static function clear()
{
q(
"DELETE FROM cache WHERE updated < '%s'",
dbesc(datetime_convert('UTC', 'UTC', "now - 30 days"))
);
}
}

2532
Code/Lib/Channel.php Normal file

File diff suppressed because it is too large Load diff

315
Code/Lib/Chatroom.php Normal file
View file

@ -0,0 +1,315 @@
<?php
namespace Code\Lib;
use Code\Lib\Libsync;
use Code\Lib\ServiceClass;
use Code\Extend\Hook;
/**
* @brief A class with chatroom related static methods.
*/
class Chatroom
{
/**
* @brief Creates a chatroom.
*
* @param array $channel
* @param array $arr
* @return array An associative array containing:
* * \e boolean \b success - A boolean success status
* * \e string \b message - (optional) A string
*/
public static function create($channel, $arr)
{
$ret = array('success' => false);
$name = trim($arr['name']);
if (!$name) {
$ret['message'] = t('Missing room name');
return $ret;
}
$r = q(
"select cr_id from chatroom where cr_uid = %d and cr_name = '%s' limit 1",
intval($channel['channel_id']),
dbesc($name)
);
if ($r) {
$ret['message'] = t('Duplicate room name');
return $ret;
}
$r = q(
"select count(cr_id) as total from chatroom where cr_aid = %d",
intval($channel['channel_account_id'])
);
if ($r) {
$limit = ServiceClass::fetch($channel['channel_id'], 'chatrooms');
}
if (($r) && ($limit !== false) && ($r[0]['total'] >= $limit)) {
$ret['message'] = ServiceClass::upgrade_message();
return $ret;
}
if (!array_key_exists('expire', $arr)) {
$arr['expire'] = 120; // minutes, e.g. 2 hours
}
$created = datetime_convert();
$x = q(
"insert into chatroom ( cr_aid, cr_uid, cr_name, cr_created, cr_edited, cr_expire, allow_cid, allow_gid, deny_cid, deny_gid )
values ( %d, %d , '%s', '%s', '%s', %d, '%s', '%s', '%s', '%s' ) ",
intval($channel['channel_account_id']),
intval($channel['channel_id']),
dbesc($name),
dbesc($created),
dbesc($created),
intval($arr['expire']),
dbesc($arr['allow_cid']),
dbesc($arr['allow_gid']),
dbesc($arr['deny_cid']),
dbesc($arr['deny_gid'])
);
if ($x) {
$ret['success'] = true;
}
return $ret;
}
public static function destroy($channel, $arr)
{
$ret = array('success' => false);
if (intval($arr['cr_id'])) {
$sql_extra = " and cr_id = " . intval($arr['cr_id']) . " ";
} elseif (trim($arr['cr_name'])) {
$sql_extra = " and cr_name = '" . protect_sprintf(dbesc(trim($arr['cr_name']))) . "' ";
} else {
$ret['message'] = t('Invalid room specifier.');
return $ret;
}
$r = q(
"select * from chatroom where cr_uid = %d $sql_extra limit 1",
intval($channel['channel_id'])
);
if (!$r) {
$ret['message'] = t('Invalid room specifier.');
return $ret;
}
Libsync::build_sync_packet($channel['channel_id'], array('chatroom' => $r));
q(
"delete from chatroom where cr_id = %d",
intval($r[0]['cr_id'])
);
if ($r[0]['cr_id']) {
q(
"delete from chatpresence where cp_room = %d",
intval($r[0]['cr_id'])
);
q(
"delete from chat where chat_room = %d",
intval($r[0]['cr_id'])
);
}
$ret['success'] = true;
return $ret;
}
public static function enter($observer_xchan, $room_id, $status, $client)
{
if (!$room_id || !$observer_xchan) {
return;
}
$r = q(
"select * from chatroom where cr_id = %d limit 1",
intval($room_id)
);
if (!$r) {
notice(t('Room not found.') . EOL);
return false;
}
require_once('include/security.php');
$sql_extra = permissions_sql($r[0]['cr_uid']);
$x = q(
"select * from chatroom where cr_id = %d and cr_uid = %d $sql_extra limit 1",
intval($room_id),
intval($r[0]['cr_uid'])
);
if (!$x) {
notice(t('Permission denied.') . EOL);
return false;
}
$limit = ServiceClass::fetch($r[0]['cr_uid'], 'chatters_inroom');
if ($limit !== false) {
$y = q(
"select count(*) as total from chatpresence where cp_room = %d",
intval($room_id)
);
if ($y && $y[0]['total'] > $limit) {
notice(t('Room is full') . EOL);
return false;
}
}
if (intval($x[0]['cr_expire'])) {
$r = q(
"delete from chat where created < %s - INTERVAL %s and chat_room = %d",
db_utcnow(),
db_quoteinterval(intval($x[0]['cr_expire']) . ' MINUTE'),
intval($x[0]['cr_id'])
);
}
$r = q(
"select * from chatpresence where cp_xchan = '%s' and cp_room = %d limit 1",
dbesc($observer_xchan),
intval($room_id)
);
if ($r) {
q(
"update chatpresence set cp_last = '%s' where cp_id = %d and cp_client = '%s'",
dbesc(datetime_convert()),
intval($r[0]['cp_id']),
dbesc($client)
);
return true;
}
$r = q(
"insert into chatpresence ( cp_room, cp_xchan, cp_last, cp_status, cp_client )
values ( %d, '%s', '%s', '%s', '%s' )",
intval($room_id),
dbesc($observer_xchan),
dbesc(datetime_convert()),
dbesc($status),
dbesc($client)
);
return $r;
}
public function leave($observer_xchan, $room_id, $client)
{
if (!$room_id || !$observer_xchan) {
return;
}
$r = q(
"select * from chatpresence where cp_xchan = '%s' and cp_room = %d and cp_client = '%s' limit 1",
dbesc($observer_xchan),
intval($room_id),
dbesc($client)
);
if ($r) {
q(
"delete from chatpresence where cp_id = %d",
intval($r[0]['cp_id'])
);
}
return true;
}
public static function roomlist($uid)
{
require_once('include/security.php');
$sql_extra = permissions_sql($uid);
$r = q(
"select allow_cid, allow_gid, deny_cid, deny_gid, cr_name, cr_expire, cr_id, count(cp_id) as cr_inroom from chatroom left join chatpresence on cr_id = cp_room where cr_uid = %d $sql_extra group by cr_name, cr_id order by cr_name",
intval($uid)
);
return $r;
}
public static function list_count($uid)
{
require_once('include/security.php');
$sql_extra = permissions_sql($uid);
$r = q(
"select count(*) as total from chatroom where cr_uid = %d $sql_extra",
intval($uid)
);
return $r[0]['total'];
}
/**
* @brief Create a chat message via API.
*
* It is the caller's responsibility to enter the room.
*
* @param int $uid
* @param int $room_id
* @param string $xchan
* @param string $text
* @return array
*/
public static function message($uid, $room_id, $xchan, $text)
{
$ret = array('success' => false);
if (!$text) {
return;
}
$sql_extra = permissions_sql($uid);
$r = q(
"select * from chatroom where cr_uid = %d and cr_id = %d $sql_extra",
intval($uid),
intval($room_id)
);
if (!$r) {
return $ret;
}
$arr = [
'chat_room' => $room_id,
'chat_xchan' => $xchan,
'chat_text' => $text
];
/**
* @hooks chat_message
* Called to create a chat message.
* * \e int \b chat_room
* * \e string \b chat_xchan
* * \e string \b chat_text
*/
Hook::call('chat_message', $arr);
$x = q(
"insert into chat ( chat_room, chat_xchan, created, chat_text )
values( %d, '%s', '%s', '%s' )",
intval($room_id),
dbesc($xchan),
dbesc(datetime_convert()),
dbesc(str_rot47(base64url_encode($arr['chat_text'])))
);
$ret['success'] = true;
return $ret;
}
}

175
Code/Lib/Config.php Normal file
View file

@ -0,0 +1,175 @@
<?php
namespace Code\Lib;
use App;
class Config
{
/**
* @brief Loads the hub's configuration from database to a cached storage.
*
* Retrieve a category ($family) of config variables from database to a cached
* storage in the global App::$config[$family].
*
* @param string $family
* The category of the configuration value
*/
public static function Load($family)
{
if (! array_key_exists($family, App::$config)) {
App::$config[$family] = [];
}
if (! array_key_exists('config_loaded', App::$config[$family])) {
$r = q("SELECT * FROM config WHERE cat = '%s'", dbesc($family));
if ($r !== false) {
if ($r) {
foreach ($r as $rr) {
$k = $rr['k'];
App::$config[$family][$k] = $rr['v'];
}
}
App::$config[$family]['config_loaded'] = true;
}
}
}
/**
* @brief Sets a configuration value for the hub.
*
* Stores a config value ($value) in the category ($family) under the key ($key).
*
* @param string $family
* The category of the configuration value
* @param string $key
* The configuration key to set
* @param mixed $value
* The value to store in the configuration
* @return mixed
* Return the set value, or false if the database update failed
*/
public static function Set($family, $key, $value)
{
// manage array value
$dbvalue = ((is_array($value)) ? serialise($value) : $value);
$dbvalue = ((is_bool($dbvalue)) ? intval($dbvalue) : $dbvalue);
if (self::Get($family, $key) === false || (! self::get_from_storage($family, $key))) {
$ret = q(
"INSERT INTO config ( cat, k, v ) VALUES ( '%s', '%s', '%s' ) ",
dbesc($family),
dbesc($key),
dbesc($dbvalue)
);
if ($ret) {
App::$config[$family][$key] = $value;
$ret = $value;
}
return $ret;
}
$ret = q(
"UPDATE config SET v = '%s' WHERE cat = '%s' AND k = '%s'",
dbesc($dbvalue),
dbesc($family),
dbesc($key)
);
if ($ret) {
App::$config[$family][$key] = $value;
$ret = $value;
}
return $ret;
}
/**
* @brief Get a particular config variable given the category name ($family)
* and a key.
*
* Get a particular config variable from the given category ($family) and the
* $key from a cached storage in App::$config[$family]. If a key is found in the
* DB but does not exist in local config cache, pull it into the cache so we
* do not have to hit the DB again for this item.
*
* Returns false if not set.
*
* @param string $family
* The category of the configuration value
* @param string $key
* The configuration key to query
* @param string $default (optional) default false
* @return mixed Return value or false on error or if not set
*/
public static function Get($family, $key, $default = false)
{
if ((! array_key_exists($family, App::$config)) || (! array_key_exists('config_loaded', App::$config[$family]))) {
self::Load($family);
}
if (array_key_exists('config_loaded', App::$config[$family])) {
if (! array_key_exists($key, App::$config[$family])) {
return $default;
}
return unserialise(App::$config[$family][$key]);
}
return $default;
}
/**
* @brief Deletes the given key from the hub's configuration database.
*
* Removes the configured value from the stored cache in App::$config[$family]
* and removes it from the database.
*
* @param string $family
* The category of the configuration value
* @param string $key
* The configuration key to delete
* @return mixed
*/
public static function Delete($family, $key)
{
$ret = false;
if (array_key_exists($family, App::$config) && array_key_exists($key, App::$config[$family])) {
unset(App::$config[$family][$key]);
}
$ret = q(
"DELETE FROM config WHERE cat = '%s' AND k = '%s'",
dbesc($family),
dbesc($key)
);
return $ret;
}
/**
* @brief Returns a record directly from the database configuration storage.
*
* This function queries directly the database and bypasses the cached storage
* from get_config($family, $key).
*
* @param string $family
* The category of the configuration value
* @param string $key
* The configuration key to query
* @return mixed
*/
private static function get_from_storage($family, $key)
{
$ret = q(
"SELECT * FROM config WHERE cat = '%s' AND k = '%s' LIMIT 1",
dbesc($family),
dbesc($key)
);
return $ret;
}
}

311
Code/Lib/Connect.php Normal file
View file

@ -0,0 +1,311 @@
<?php
namespace Code\Lib;
use App;
use Code\Lib\ServiceClass;
use Code\Access\Permissions;
use Code\Daemon\Run;
use Code\Extend\Hook;
class Connect
{
/**
* Takes a $channel and a $url/handle and adds a new connection
*
* Returns array
* $return['success'] boolean true if successful
* $return['abook'] Address book entry joined with xchan if successful
* $return['message'] error text if success is false.
*
* This function does NOT send sync packets to clones. The caller is responsible for doing this
*/
public static function connect($channel, $url, $sub_channel = false)
{
$uid = $channel['channel_id'];
if (strpos($url, '@') === false && strpos($url, '/') === false) {
$url = $url . '@' . App::get_hostname();
}
$result = ['success' => false, 'message' => ''];
$my_perms = false;
$protocol = '';
$ap_allowed = get_config('system', 'activitypub', ACTIVITYPUB_ENABLED) && get_pconfig($uid, 'system', 'activitypub', ACTIVITYPUB_ENABLED);
if (substr($url, 0, 1) === '[') {
$x = strpos($url, ']');
if ($x) {
$protocol = substr($url, 1, $x - 1);
$url = substr($url, $x + 1);
}
}
if (!check_siteallowed($url)) {
$result['message'] = t('Channel is blocked on this site.');
return $result;
}
if (!$url) {
$result['message'] = t('Channel location missing.');
return $result;
}
// check service class limits
$r = q(
"select count(*) as total from abook where abook_channel = %d and abook_self = 0 ",
intval($uid)
);
if ($r) {
$total_channels = $r[0]['total'];
}
if (!ServiceClass::allows($uid, 'total_channels', $total_channels)) {
$result['message'] = ServiceClass::upgrade_message();
return $result;
}
$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 ",
dbesc($url),
dbesc($url),
dbesc($url)
);
if ($r) {
// reset results to the best record or the first if we don't have the best
// note: this returns a single record and not an array of records
$r = Libzot::zot_record_preferred($r, 'xchan_network');
// ensure there's a valid hubloc for this xchan before proceeding - you cannot connect without it
if (in_array($r['xchan_network'], ['nomad', 'zot6', 'activitypub'])) {
$h = q(
"select * from hubloc where hubloc_hash = '%s'",
dbesc($r['xchan_hash'])
);
if (!$h) {
$r = null;
}
}
// we may have nulled out this record so check again
if ($r) {
// Check the site table to see if we should have a zot6 hubloc,
// If so, clear the xchan and start fresh
if ($r['xchan_network'] === 'activitypub') {
$m = parse_url($r['xchan_hash']);
unset($m['path']);
$h = unparse_url($m);
$s = q(
"select * from site where site_url = '%s'",
dbesc($h)
);
if (intval($s['site_type']) === SITE_TYPE_ZOT) {
logger('got zot - ignore activitypub entry');
$r = null;
}
}
}
}
$singleton = false;
if (!$r) {
// not in cache - try discovery
$wf = discover_by_webbie($url, $protocol, false);
if (!$wf) {
$result['message'] = t('Remote channel or protocol unavailable.');
return $result;
}
}
if ($wf) {
// 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",
dbesc(($wf) ? $wf : $url),
dbesc($url),
dbesc($url)
);
// convert to a single record (once again preferring a zot solution in the case of multiples)
if ($r) {
$r = Libzot::zot_record_preferred($r, 'xchan_network');
}
}
// if discovery was a success or the channel was already cached we should have an xchan record in $r
if ($r) {
$xchan = $r;
$xchan_hash = $r['xchan_hash'];
$their_perms = EMPTY_STR;
}
// failure case
if (!$xchan_hash) {
$result['message'] = t('Channel discovery failed.');
logger('follow: ' . $result['message']);
return $result;
}
if (!check_channelallowed($xchan_hash)) {
$result['message'] = t('Channel is blocked on this site.');
logger('follow: ' . $result['message']);
return $result;
}
if ($r['xchan_network'] === 'activitypub') {
if (!$ap_allowed) {
$result['message'] = t('Protocol not supported');
return $result;
}
$singleton = true;
}
// Now start processing the new connection
$aid = $channel['channel_account_id'];
$hash = $channel['channel_hash'];
$default_group = $channel['channel_default_group'];
if ($hash === $xchan_hash) {
$result['message'] = t('Cannot connect to yourself.');
return $result;
}
$p = Permissions::connect_perms($uid);
// parent channels have unencumbered write permission
if ($sub_channel) {
$p['perms']['post_wall'] = 1;
$p['perms']['post_comments'] = 1;
$p['perms']['write_storage'] = 1;
$p['perms']['post_like'] = 1;
$p['perms']['delegate'] = 0;
$p['perms']['moderated'] = 0;
}
$my_perms = Permissions::serialise($p['perms']);
$profile_assign = get_pconfig($uid, 'system', 'profile_assign', '');
// See if we are already connected by virtue of having an abook record
$r = q(
"select abook_id, abook_xchan, abook_pending, abook_instance from abook
where abook_xchan = '%s' and abook_channel = %d limit 1",
dbesc($xchan_hash),
intval($uid)
);
if ($r) {
$abook_instance = $r[0]['abook_instance'];
// If they are on a non-nomadic network, add them to this location
if (($singleton) && strpos($abook_instance, z_root()) === false) {
if ($abook_instance) {
$abook_instance .= ',';
}
$abook_instance .= z_root();
$x = q(
"update abook set abook_instance = '%s', abook_not_here = 0 where abook_id = %d",
dbesc($abook_instance),
intval($r[0]['abook_id'])
);
}
// if they have a pending connection, we just followed them so approve the connection request
if (intval($r[0]['abook_pending'])) {
$x = q(
"update abook set abook_pending = 0 where abook_id = %d",
intval($r[0]['abook_id'])
);
}
} else {
// create a new abook record
$closeness = get_pconfig($uid, 'system', 'new_abook_closeness', 80);
$r = abook_store_lowlevel(
[
'abook_account' => intval($aid),
'abook_channel' => intval($uid),
'abook_closeness' => intval($closeness),
'abook_xchan' => $xchan_hash,
'abook_profile' => $profile_assign,
'abook_feed' => intval(($xchan['xchan_network'] === 'rss') ? 1 : 0),
'abook_created' => datetime_convert(),
'abook_updated' => datetime_convert(),
'abook_instance' => (($singleton) ? z_root() : '')
]
);
}
if (!$r) {
logger('abook creation failed');
$result['message'] = t('error saving data');
return $result;
}
// Set suitable permissions to the connection
if ($my_perms) {
set_abconfig($uid, $xchan_hash, 'system', 'my_perms', $my_perms);
}
// fetch the entire record
$r = q(
"select abook.*, xchan.* from abook left join xchan on abook_xchan = xchan_hash
where abook_xchan = '%s' and abook_channel = %d limit 1",
dbesc($xchan_hash),
intval($uid)
);
if ($r) {
$result['abook'] = array_shift($r);
Run::Summon(['Notifier', 'permissions_create', $result['abook']['abook_id']]);
}
$arr = ['channel_id' => $uid, 'channel' => $channel, 'abook' => $result['abook']];
Hook::call('follow', $arr);
/** If there is a default group for this channel, add this connection to it */
if ($default_group) {
$g = AccessList::rec_byhash($uid, $default_group);
if ($g) {
AccessList::member_add($uid, '', $xchan_hash, $g['id']);
}
}
$result['success'] = true;
return $result;
}
}

214
Code/Lib/Crypto.php Normal file
View file

@ -0,0 +1,214 @@
<?php
namespace Code\Lib;
use Exception;
use Code\Extend\Hook;
class Crypto
{
public static $openssl_algorithms = [
// zot6 nickname, opensslname, keylength, ivlength
[ 'aes256ctr', 'aes-256-ctr', 32, 16 ],
[ 'camellia256cfb', 'camellia-256-cfb', 32, 16 ],
[ 'cast5cfb', 'cast5-cfb', 16, 8 ]
];
public static function methods()
{
$ret = [];
foreach (self::$openssl_algorithms as $ossl) {
$ret[] = $ossl[0] . '.oaep';
}
Hook::call('crypto_methods', $ret);
return $ret;
}
public static function signing_methods()
{
$ret = [ 'sha256' ];
Hook::call('signing_methods', $ret);
return $ret;
}
public static function new_keypair($bits)
{
$openssl_options = [
'digest_alg' => 'sha1',
'private_key_bits' => $bits,
'encrypt_key' => false
];
$conf = get_config('system', 'openssl_conf_file');
if ($conf) {
$openssl_options['config'] = $conf;
}
$result = openssl_pkey_new($openssl_options);
if (empty($result)) {
return false;
}
// Get private key
$response = [ 'prvkey' => '', 'pubkey' => '' ];
openssl_pkey_export($result, $response['prvkey']);
// Get public key
$pkey = openssl_pkey_get_details($result);
$response['pubkey'] = $pkey["key"];
return $response;
}
public static function sign($data, $key, $alg = 'sha256')
{
if (! $key) {
return false;
}
$sig = '';
openssl_sign($data, $sig, $key, $alg);
return $sig;
}
public static function verify($data, $sig, $key, $alg = 'sha256')
{
if (! $key) {
return false;
}
try {
$verify = openssl_verify($data, $sig, $key, $alg);
} catch (Exception $e) {
$verify = (-1);
}
if ($verify === (-1)) {
while ($msg = openssl_error_string()) {
logger('openssl_verify: ' . $msg, LOGGER_NORMAL, LOG_ERR);
}
btlogger('openssl_verify: key: ' . $key, LOGGER_DEBUG, LOG_ERR);
}
return (($verify > 0) ? true : false);
}
public static function encapsulate($data, $pubkey, $alg)
{
if (! ($alg && $pubkey)) {
return $data;
}
$alg_base = $alg;
$padding = OPENSSL_PKCS1_PADDING;
$exts = explode('.', $alg);
if (count($exts) > 1) {
switch ($exts[1]) {
case 'oaep':
$padding = OPENSSL_PKCS1_OAEP_PADDING;
break;
default:
break;
}
$alg_base = $exts[0];
}
$method = null;
foreach (self::$openssl_algorithms as $ossl) {
if ($ossl[0] === $alg_base) {
$method = $ossl;
break;
}
}
if ($method) {
$result = [ 'encrypted' => true ];
$key = openssl_random_pseudo_bytes(256);
$iv = openssl_random_pseudo_bytes(256);
$key1 = substr($key, 0, $method[2]);
$iv1 = substr($iv, 0, $method[3]);
$result['data'] = base64url_encode(openssl_encrypt($data, $method[1], $key1, OPENSSL_RAW_DATA, $iv1), true);
openssl_public_encrypt($key, $k, $pubkey, $padding);
openssl_public_encrypt($iv, $i, $pubkey, $padding);
$result['alg'] = $alg;
$result['key'] = base64url_encode($k, true);
$result['iv'] = base64url_encode($i, true);
return $result;
} else {
$x = [ 'data' => $data, 'pubkey' => $pubkey, 'alg' => $alg, 'result' => $data ];
Hook::call('crypto_encapsulate', $x);
return $x['result'];
}
}
public static function unencapsulate($data, $prvkey)
{
if (! (is_array($data) && array_key_exists('encrypted', $data) && array_key_exists('alg', $data) && $data['alg'])) {
logger('not encrypted');
return $data;
}
$alg_base = $data['alg'];
$padding = OPENSSL_PKCS1_PADDING;
$exts = explode('.', $data['alg']);
if (count($exts) > 1) {
switch ($exts[1]) {
case 'oaep':
$padding = OPENSSL_PKCS1_OAEP_PADDING;
break;
default:
break;
}
$alg_base = $exts[0];
}
$method = null;
foreach (self::$openssl_algorithms as $ossl) {
if ($ossl[0] === $alg_base) {
$method = $ossl;
break;
}
}
if ($method) {
openssl_private_decrypt(base64url_decode($data['key']), $k, $prvkey, $padding);
openssl_private_decrypt(base64url_decode($data['iv']), $i, $prvkey, $padding);
return openssl_decrypt(base64url_decode($data['data']), $method[1], substr($k, 0, $method[2]), OPENSSL_RAW_DATA, substr($i, 0, $method[3]));
} else {
$x = [ 'data' => $data, 'prvkey' => $prvkey, 'alg' => $data['alg'], 'result' => $data ];
Hook::call('crypto_unencapsulate', $x);
return $x['result'];
}
}
}

119
Code/Lib/DB_Upgrade.php Normal file
View file

@ -0,0 +1,119 @@
<?php
namespace Code\Lib;
use App;
use Code\Render\Theme;
class DB_Upgrade
{
public $config_name = '';
public $func_prefix = '';
public function __construct($db_revision)
{
$this->config_name = 'db_version';
$this->func_prefix = '_';
$build = get_config('system', 'db_version', 0);
if (!intval($build)) {
$build = set_config('system', 'db_version', $db_revision);
}
if ($build == $db_revision) {
// Nothing to be done.
return;
} else {
$stored = intval($build);
if (!$stored) {
logger('Critical: check_config unable to determine database schema version');
return;
}
$current = intval($db_revision);
if ($stored < $current) {
// The last update we performed was $stored.
// Start at $stored + 1 and continue until we have completed $current
for ($x = $stored + 1; $x <= $current; $x++) {
$s = '_' . $x;
$cls = '\\Code\Update\\' . $s;
if (!class_exists($cls)) {
return;
}
// There could be a lot of processes running or about to run.
// We want exactly one process to run the update command.
// So store the fact that we're taking responsibility
// after first checking to see if somebody else already has.
// If the update fails or times-out completely you may need to
// delete the config entry to try again.
Config::Load('database');
if (get_config('database', $s)) {
break;
}
set_config('database', $s, '1');
$c = new $cls();
$retval = $c->run();
if ($retval != UPDATE_SUCCESS) {
$source = t('Source code of failed update: ') . "\n\n" . @file_get_contents('Code/Update/' . $s . '.php');
// Prevent sending hundreds of thousands of emails by creating
// a lockfile.
$lockfile = 'cache/mailsent';
if ((file_exists($lockfile)) && (filemtime($lockfile) > (time() - 86400))) {
return;
}
@unlink($lockfile);
//send the administrator an e-mail
file_put_contents($lockfile, $x);
$r = q(
"select account_language from account where account_email = '%s' limit 1",
dbesc(App::$config['system']['admin_email'])
);
push_lang(($r) ? $r[0]['account_language'] : 'en');
z_mail(
[
'toEmail' => App::$config['system']['admin_email'],
'messageSubject' => sprintf(t('Update Error at %s'), z_root()),
'textVersion' => replace_macros(
Theme::get_email_template('update_fail_eml.tpl'),
[
'$sitename' => App::$config['system']['sitename'],
'$siteurl' => z_root(),
'$update' => $x,
'$error' => sprintf(t('Update %s failed. See error logs.'), $x),
'$baseurl' => z_root(),
'$source' => $source
]
)
]
);
//try the logger
logger('CRITICAL: Update Failed: ' . $x);
pop_lang();
} else {
set_config('database', $s, 'success');
}
}
}
set_config('system', 'db_version', $db_revision);
}
}
}

156
Code/Lib/DReport.php Normal file
View file

@ -0,0 +1,156 @@
<?php
namespace Code\Lib;
use Code\Extend\Hook;
class DReport
{
private $location;
private $sender;
private $recipient;
private $message_id;
private $status;
private $date;
public function __construct($location, $sender, $recipient, $message_id, $status = 'deliver')
{
$this->location = $location;
$this->sender = $sender;
$this->recipient = $recipient;
$this->name = EMPTY_STR;
$this->message_id = $message_id;
$this->status = $status;
$this->date = datetime_convert();
}
public function update($status)
{
$this->status = $status;
$this->date = datetime_convert();
}
public function set_name($name)
{
$this->name = $name;
}
public function addto_update($status)
{
$this->status = $this->status . ' ' . $status;
}
public function set($arr)
{
$this->location = $arr['location'];
$this->sender = $arr['sender'];
$this->recipient = $arr['recipient'];
$this->name = $arr['name'];
$this->message_id = $arr['message_id'];
$this->status = $arr['status'];
$this->date = $arr['date'];
}
public function get()
{
return array(
'location' => $this->location,
'sender' => $this->sender,
'recipient' => $this->recipient,
'name' => $this->name,
'message_id' => $this->message_id,
'status' => $this->status,
'date' => $this->date
);
}
/**
* @brief decide whether to store a returned delivery report
*
* @param array $dr
* @return bool
*/
public static function is_storable($dr)
{
if (get_config('system', 'disable_dreport')) {
return false;
}
/**
* @hooks dreport_is_storable
* Called before storing a dreport record to determine whether to store it.
* * \e array
*/
Hook::call('dreport_is_storable', $dr);
// let plugins accept or reject - if neither, continue on
if (array_key_exists('accept', $dr) && intval($dr['accept'])) {
return true;
}
if (array_key_exists('reject', $dr) && intval($dr['reject'])) {
return false;
}
if (!($dr['sender'])) {
return false;
}
// Is the sender one of our channels?
$c = q(
"select channel_id from channel where channel_hash = '%s' limit 1",
dbesc($dr['sender'])
);
if (!$c) {
return false;
}
// is the recipient one of our connections, or do we want to store every report?
$rxchan = $dr['recipient'];
$pcf = get_pconfig($c[0]['channel_id'], 'system', 'dreport_store_all');
if ($pcf) {
return true;
}
// We always add ourself as a recipient to private and relayed posts
// So if a remote site says they can't find us, that's no big surprise
// and just creates a lot of extra report noise
if (($dr['location'] !== z_root()) && ($dr['sender'] === $rxchan) && ($dr['status'] === 'recipient not found')) {
return false;
}
// If you have a private post with a recipient list, every single site is going to report
// back a failed delivery for anybody on that list that isn't local to them. We're only
// concerned about this if we have a local hubloc record which says we expected them to
// have a channel on that site.
$r = q(
"select hubloc_id from hubloc where hubloc_hash = '%s' and hubloc_url = '%s'",
dbesc($rxchan),
dbesc($dr['location'])
);
if ((!$r) && ($dr['status'] === 'recipient not found')) {
return false;
}
$r = q(
"select abook_id from abook where abook_xchan = '%s' and abook_channel = %d limit 1",
dbesc($rxchan),
intval($c[0]['channel_id'])
);
if ($r) {
return true;
}
return false;
}
}

985
Code/Lib/Enotify.php Normal file
View file

@ -0,0 +1,985 @@
<?php
namespace Code\Lib;
use App;
use Code\Lib\LibBlock;
use Code\Lib\System;
use Code\Lib\Channel;
use Code\Extend\Hook;
use Code\Render\Theme;
/**
* @brief File with functions and a class for generating system and email notifications.
*/
class Enotify
{
/**
* @brief
*
* @param array $params an associative array with:
* * \e string \b from_xchan sender xchan hash
* * \e string \b to_xchan recipient xchan hash
* * \e array \b item an assoziative array
* * \e int \b type one of the NOTIFY_* constants from boot.php
* * \e string \b link
* * \e string \b parent_mid
* * \e string \b otype
* * \e string \b verb
* * \e string \b activity
*/
public static function submit($params)
{
logger('notification: entry', LOGGER_DEBUG);
// throw a small amount of entropy into the system to breakup duplicates arriving at the same precise instant.
usleep(mt_rand(0, 10000));
if ($params['from_xchan']) {
$x = q(
"select * from xchan where xchan_hash = '%s' limit 1",
dbesc($params['from_xchan'])
);
}
if ($params['to_xchan']) {
$y = q(
"select channel.*, account.* from channel left join account on channel_account_id = account_id
where channel_hash = '%s' and channel_removed = 0 limit 1",
dbesc($params['to_xchan'])
);
}
if ($x && $y) {
$sender = $x[0];
$recip = $y[0];
} else {
logger('notification: no sender or recipient.');
logger('sender: ' . $params['from_xchan']);
logger('recip: ' . $params['to_xchan']);
return;
}
// from here on everything is in the recipients language
push_lang($recip['account_language']); // should probably have a channel language
$banner = t('$Projectname Notification');
$product = t('$projectname'); // PLATFORM_NAME;
$siteurl = z_root();
$thanks = t('Thank You,');
$sitename = get_config('system', 'sitename');
$site_admin = sprintf(t('%s Administrator'), $sitename);
$opt_out1 = sprintf(t('This email was sent by %1$s at %2$s.'), t('$Projectname'), App::get_hostname());
$opt_out2 = sprintf(t('To stop receiving these messages, please adjust your Notification Settings at %s'), z_root() . '/settings');
$hopt_out2 = sprintf(t('To stop receiving these messages, please adjust your %s.'), '<a href="' . z_root() . '/settings' . '">' . t('Notification Settings') . '</a>');
$sender_name = $product;
$hostname = App::get_hostname();
if (strpos($hostname, ':')) {
$hostname = substr($hostname, 0, strpos($hostname, ':'));
}
// Do not translate 'noreply' as it must be a legal 7-bit email address
$reply_email = get_config('system', 'reply_address');
if (! $reply_email) {
$reply_email = 'noreply' . '@' . $hostname;
}
$sender_email = get_config('system', 'from_email');
if (! $sender_email) {
$sender_email = 'Administrator' . '@' . $hostname;
}
$sender_name = get_config('system', 'from_email_name');
if (! $sender_name) {
$sender_name = System::get_site_name();
}
$additional_mail_header = "";
if (array_key_exists('item', $params)) {
require_once('include/conversation.php');
// if it's a normal item...
if (array_key_exists('verb', $params['item'])) {
// localize_item() alters the original item so make a copy first
$i = $params['item'];
logger('calling localize');
localize_item($i);
$title = $i['title'];
$body = $i['body'];
$private = (($i['item_private']) || intval($i['item_obscured']));
} else {
$title = $params['item']['title'];
$body = $params['item']['body'];
}
if ($params['item']['created'] < datetime_convert('UTC', 'UTC', 'now - 1 month')) {
logger('notification invoked for an old item which may have been refetched.', LOGGER_DEBUG, LOG_INFO);
return;
}
} else {
$title = $body = '';
}
$always_show_in_notices = get_pconfig($recip['channel_id'], 'system', 'always_show_in_notices');
$vnotify = get_pconfig($recip['channel_id'], 'system', 'vnotify');
$salutation = $recip['channel_name'];
// e.g. "your post", "David's photo", etc.
$possess_desc = t('%s <!item_type!>');
if ($params['type'] == NOTIFY_MAIL) {
logger('notification: mail');
$subject = sprintf(t('[$Projectname:Notify] New mail received at %s'), $sitename);
if ($params['item']['mid'] === $params['item']['parent_mid']) {
$preamble = sprintf(t('%1$s sent you a new private message at %2$s.'), $sender['xchan_name'], $sitename);
$epreamble = sprintf(t('%1$s sent you %2$s.'), '[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]', '[zrl=$itemlink]' . t('a private message') . '[/zrl]');
} else {
$preamble = sprintf(t('%1$s replied to a private message at %2$s.'), $sender['xchan_name'], $sitename);
$epreamble = sprintf(t('%1$s replied to %2$s.'), '[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]', '[zrl=$itemlink]' . t('a private message') . '[/zrl]');
}
$sitelink = t('Please visit %s to view and/or reply to your private messages.');
$tsitelink = sprintf($sitelink, $siteurl . '/display/' . gen_link_id($params['item']['mid']));
$hsitelink = sprintf($sitelink, '<a href="' . $siteurl . '/display/' . gen_link_id($params['item']['mid']) . '">' . $sitename . '</a>');
$itemlink = z_root() . '/display/' . gen_link_id($params['item']['mid']);
}
if (in_array(intval($params['type']), [ NOTIFY_COMMENT, NOTIFY_RESHARE ])) {
// logger("notification: params = " . print_r($params, true), LOGGER_DEBUG);
$moderated = (($params['item']['item_blocked'] == ITEM_MODERATED) ? true : false);
$itemlink = $params['link'];
$action = t('commented on');
if (array_key_exists('item', $params) && in_array($params['item']['verb'], [ACTIVITY_LIKE, ACTIVITY_DISLIKE])) {
if (! $always_show_in_notices || !($vnotify & VNOTIFY_LIKE)) {
logger('notification: not a visible activity. Ignoring.');
pop_lang();
return;
}
if (activity_match($params['verb'], ACTIVITY_LIKE)) {
$action = t('liked');
}
if (activity_match($params['verb'], ACTIVITY_DISLIKE)) {
$action = t('disliked');
}
}
$parent_mid = $params['parent_mid'];
// Check to see if there was already a notify for this post.
// If so don't create a second notification
$p = null;
$p = q(
"select id from notify where link = '%s' and uid = %d limit 1",
dbesc($params['link']),
intval($recip['channel_id'])
);
if ($p) {
logger('notification: comment already notified');
pop_lang();
return;
}
// if it's a post figure out who's post it is.
$p = null;
if ($params['otype'] === 'item' && $parent_mid) {
$p = q(
"select * from item where mid = '%s' and uid = %d limit 1",
dbesc($parent_mid),
intval($recip['channel_id'])
);
}
xchan_query($p);
$item_post_type = item_post_type($p[0]);
// $private = $p[0]['item_private'];
$parent_id = $p[0]['id'];
$parent_item = $p[0];
//$possess_desc = str_replace('<!item_type!>',$possess_desc);
// "a post"
$dest_str = sprintf(
t('%1$s %2$s [zrl=%3$s]a %4$s[/zrl]'),
'[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]',
$action,
$itemlink,
$item_post_type
);
// "George Bull's post"
if ($p) {
$dest_str = sprintf(
t('%1$s %2$s [zrl=%3$s]%4$s\'s %5$s[/zrl]'),
'[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]',
$action,
$itemlink,
$p[0]['author']['xchan_name'],
$item_post_type
);
}
// "your post"
if ($p[0]['owner']['xchan_name'] == $p[0]['author']['xchan_name'] && intval($p[0]['item_wall'])) {
$dest_str = sprintf(
t('%1$s %2$s [zrl=%3$s]your %4$s[/zrl]'),
'[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]',
$action,
$itemlink,
$item_post_type
);
}
// Some mail softwares relies on subject field for threading.
// So, we cannot have different subjects for notifications of the same thread.
// Before this we have the name of the replier on the subject rendering
// differents subjects for messages on the same thread.
if ($moderated) {
$subject = sprintf(t('[$Projectname:Notify] Moderated Comment to conversation #%1$d by %2$s'), $parent_id, $sender['xchan_name']);
$itemlink = z_root() . '/moderate/' . gen_link_id($params['item']['mid']);
} else {
$subject = sprintf(t('[$Projectname:Notify] Comment to conversation #%1$d by %2$s'), $parent_id, $sender['xchan_name']);
}
$preamble = sprintf(t('%1$s commented on an item/conversation you have been following.'), $sender['xchan_name']);
$epreamble = $dest_str;
if ($moderated) {
$epreamble .= ' ' . t('(Moderated)');
}
$sitelink = t('Please visit %s to view and/or reply to the conversation.');
$tsitelink = sprintf($sitelink, $siteurl);
$hsitelink = sprintf($sitelink, '<a href="' . $siteurl . '">' . $sitename . '</a>');
if ($moderated) {
$tsitelink .= "\n\n" . sprintf(t('Please visit %s to approve or reject this comment.'), z_root() . '/moderate');
$hsitelink .= "<br><br>" . sprintf(t('Please visit %s to approve or reject this comment.'), '<a href="' . z_root() . '/moderate">' . z_root() . '/moderate</a>');
}
}
if ($params['type'] == NOTIFY_LIKE) {
// logger("notification: params = " . print_r($params, true), LOGGER_DEBUG);
$itemlink = $params['link'];
if (array_key_exists('item', $params) && (! activity_match($params['item']['verb'], ACTIVITY_LIKE))) {
if (! $always_show_in_notices || !($vnotify & VNOTIFY_LIKE)) {
logger('notification: not a visible activity. Ignoring.');
pop_lang();
return;
}
}
$parent_mid = $params['parent_mid'];
// Check to see if there was already a notify for this post.
// If so don't create a second notification
$p = null;
$p = q(
"select id from notify where link = '%s' and uid = %d limit 1",
dbesc($params['link']),
intval($recip['channel_id'])
);
if ($p) {
logger('notification: like already notified');
pop_lang();
return;
}
// if it's a post figure out who's post it is.
$p = null;
if ($params['otype'] === 'item' && $parent_mid) {
$p = q(
"select * from item where mid = '%s' and uid = %d limit 1",
dbesc($parent_mid),
intval($recip['channel_id'])
);
}
xchan_query($p);
$item_post_type = item_post_type($p[0]);
// $private = $p[0]['item_private'];
$parent_id = $p[0]['id'];
$parent_item = $p[0];
// "your post"
if ($p[0]['owner']['xchan_name'] == $p[0]['author']['xchan_name'] && intval($p[0]['item_wall'])) {
$dest_str = sprintf(
t('%1$s liked [zrl=%2$s]your %3$s[/zrl]'),
'[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]',
$itemlink,
$item_post_type
);
} else {
pop_lang();
return;
}
// Some mail softwares relies on subject field for threading.
// So, we cannot have different subjects for notifications of the same thread.
// Before this we have the name of the replier on the subject rendering
// differents subjects for messages on the same thread.
$subject = sprintf(t('[$Projectname:Notify] Like received to conversation #%1$d by %2$s'), $parent_id, $sender['xchan_name']);
$preamble = sprintf(t('%1$s liked an item/conversation you created.'), $sender['xchan_name']);
$epreamble = $dest_str;
$sitelink = t('Please visit %s to view and/or reply to the conversation.');
$tsitelink = sprintf($sitelink, $siteurl);
$hsitelink = sprintf($sitelink, '<a href="' . $siteurl . '">' . $sitename . '</a>');
}
if ($params['type'] == NOTIFY_WALL) {
$subject = sprintf(t('[$Projectname:Notify] %s posted to your profile wall'), $sender['xchan_name']);
$moderated = (($params['item']['item_blocked'] == ITEM_MODERATED) ? true : false);
$itemlink = (($moderated) ? z_root() . '/moderate/' . gen_link_id($params['item']['mid']) : $params['link']);
$preamble = sprintf(t('%1$s posted to your profile wall at %2$s'), $sender['xchan_name'], $sitename);
$epreamble = sprintf(
t('%1$s posted to [zrl=%2$s]your wall[/zrl]'),
'[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]',
$itemlink
);
if ($moderated) {
$subject .= t(' - ') . t('Moderated');
$epreamble .= t(' - ') . t('Moderated');
}
$sitelink = t('Please visit %s to view and/or reply to the conversation.');
$tsitelink = sprintf($sitelink, $siteurl);
$hsitelink = sprintf($sitelink, '<a href="' . $siteurl . '">' . $sitename . '</a>');
if ($moderated) {
$tsitelink .= "\n\n" . sprintf(t('Please visit %s to approve or reject this post.'), z_root() . '/moderate');
$hsitelink .= "<br><br>" . sprintf(t('Please visit %s to approve or reject this post.'), '<a href="' . z_root() . '/moderate">' . z_root() . '/moderate</a>');
}
}
if ($params['type'] == NOTIFY_TAGSELF) {
$p = null;
$p = q(
"select id from notify where link = '%s' and uid = %d limit 1",
dbesc($params['link']),
intval($recip['channel_id'])
);
if ($p) {
logger('enotify: tag: already notified about this post');
pop_lang();
return;
}
$subject = sprintf(t('[$Projectname:Notify] %s tagged you'), $sender['xchan_name']);
$preamble = sprintf(t('%1$s tagged you at %2$s'), $sender['xchan_name'], $sitename);
$epreamble = sprintf(
t('%1$s [zrl=%2$s]tagged you[/zrl].'),
'[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]',
$params['link']
);
$sitelink = t('Please visit %s to view and/or reply to the conversation.');
$tsitelink = sprintf($sitelink, $siteurl);
$hsitelink = sprintf($sitelink, '<a href="' . $siteurl . '">' . $sitename . '</a>');
$itemlink = $params['link'];
}
if ($params['type'] == NOTIFY_POKE) {
$subject = sprintf(t('[$Projectname:Notify] %1$s poked you'), $sender['xchan_name']);
$preamble = sprintf(t('%1$s poked you at %2$s'), $sender['xchan_name'], $sitename);
$epreamble = sprintf(
t('%1$s [zrl=%2$s]poked you[/zrl].'),
'[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]',
$params['link']
);
$subject = str_replace('poked', t($params['activity']), $subject);
$preamble = str_replace('poked', t($params['activity']), $preamble);
$epreamble = str_replace('poked', t($params['activity']), $epreamble);
$sitelink = t('Please visit %s to view and/or reply to the conversation.');
$tsitelink = sprintf($sitelink, $siteurl);
$hsitelink = sprintf($sitelink, '<a href="' . $siteurl . '">' . $sitename . '</a>');
$itemlink = $params['link'];
}
if ($params['type'] == NOTIFY_TAGSHARE) {
$subject = sprintf(t('[$Projectname:Notify] %s tagged your post'), $sender['xchan_name']);
$preamble = sprintf(t('%1$s tagged your post at %2$s'), $sender['xchan_name'], $sitename);
$epreamble = sprintf(
t('%1$s tagged [zrl=%2$s]your post[/zrl]'),
'[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]',
$itemlink
);
$sitelink = t('Please visit %s to view and/or reply to the conversation.');
$tsitelink = sprintf($sitelink, $siteurl);
$hsitelink = sprintf($sitelink, '<a href="' . $siteurl . '">' . $sitename . '</a>');
$itemlink = $params['link'];
}
if ($params['type'] == NOTIFY_INTRO) {
$subject = sprintf(t('[$Projectname:Notify] Introduction received'));
$preamble = sprintf(t('You\'ve received an new connection request from \'%1$s\' at %2$s'), $sender['xchan_name'], $sitename);
$epreamble = sprintf(
t('You\'ve received [zrl=%1$s]a new connection request[/zrl] from %2$s.'),
$siteurl . '/connections/ifpending',
'[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]'
);
$body = sprintf(t('You may visit their profile at %s'), $sender['xchan_url']);
$sitelink = t('Please visit %s to approve or reject the connection request.');
$tsitelink = sprintf($sitelink, $siteurl . '/connections/ifpending');
$hsitelink = sprintf($sitelink, '<a href="' . $siteurl . '/connections/ifpending">' . $sitename . '</a>');
$itemlink = $params['link'];
}
if ($params['type'] == NOTIFY_SUGGEST) {
$subject = sprintf(t('[$Projectname:Notify] Friend suggestion received'));
$preamble = sprintf(t('You\'ve received a friend suggestion from \'%1$s\' at %2$s'), $sender['xchan_name'], $sitename);
$epreamble = sprintf(
t('You\'ve received [zrl=%1$s]a friend suggestion[/zrl] for %2$s from %3$s.'),
$itemlink,
'[zrl=' . $params['item']['url'] . ']' . $params['item']['name'] . '[/zrl]',
'[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]'
);
$body = t('Name:') . ' ' . $params['item']['name'] . "\n";
$body .= t('Photo:') . ' ' . $params['item']['photo'] . "\n";
$body .= sprintf(t('You may visit their profile at %s'), $params['item']['url']);
$sitelink = t('Please visit %s to approve or reject the suggestion.');
$tsitelink = sprintf($sitelink, $siteurl);
$hsitelink = sprintf($sitelink, '<a href="' . $siteurl . '">' . $sitename . '</a>');
$itemlink = $params['link'];
}
if ($params['type'] == NOTIFY_CONFIRM) {
// ?
}
if ($params['type'] == NOTIFY_SYSTEM) {
// ?
}
$h = array(
'params' => $params,
'subject' => $subject,
'preamble' => $preamble,
'epreamble' => $epreamble,
'body' => $body,
'sitelink' => $sitelink,
'sitename' => $sitename,
'tsitelink' => $tsitelink,
'hsitelink' => $hsitelink,
'itemlink' => $itemlink,
'sender' => $sender,
'recipient' => $recip
);
Hook::call('enotify', $h);
$subject = $h['subject'];
$preamble = $h['preamble'];
$epreamble = $h['epreamble'];
$body = $h['body'];
$sitelink = $h['sitelink'];
$tsitelink = $h['tsitelink'];
$hsitelink = $h['hsitelink'];
$itemlink = $h['itemlink'];
require_once('include/html2bbcode.php');
do {
$dups = false;
$hash = random_string();
$r = q(
"SELECT id FROM notify WHERE hash = '%s' LIMIT 1",
dbesc($hash)
);
if ($r) {
$dups = true;
}
} while ($dups === true);
$datarray = [];
$datarray['hash'] = $hash;
$datarray['sender_hash'] = $sender['xchan_hash'];
$datarray['xname'] = $sender['xchan_name'];
$datarray['url'] = $sender['xchan_url'];
$datarray['photo'] = $sender['xchan_photo_s'];
$datarray['created'] = datetime_convert();
$datarray['aid'] = $recip['channel_account_id'];
$datarray['uid'] = $recip['channel_id'];
$datarray['link'] = $itemlink;
$datarray['parent'] = $parent_mid;
$datarray['parent_item'] = $parent_item;
$datarray['ntype'] = $params['type'];
$datarray['verb'] = $params['verb'];
$datarray['otype'] = $params['otype'];
$datarray['abort'] = false;
$datarray['item'] = $params['item'];
if (LibBlock::fetch_by_entity($datarray['uid'], $datarray['sender_hash'])) {
pop_lang();
return;
}
if (is_array($datarray['parent_item'])) {
if (LibBlock::fetch_by_entity($datarray['uid'], $datarray['parent_item']['author_xchan']) || LibBlock::fetch_by_entity($datarray['uid'], $datarray['parent_item']['owner_xchan'])) {
pop_lang();
return;
}
}
Hook::call('enotify_store', $datarray);
if ($datarray['abort']) {
pop_lang();
return;
}
// create notification entry in DB
$seen = 0;
// Mark some notifications as seen right away
// Note! The notification have to be created, because they are used to send emails
// So easiest solution to hide them from Notices is to mark them as seen right away.
// Another option would be to not add them to the DB, and change how emails are handled
// (probably would be better that way)
if (!$always_show_in_notices) {
if (($params['type'] == NOTIFY_WALL) || ($params['type'] == NOTIFY_INTRO)) {
$seen = 1;
}
// set back to unseen for moderated wall posts
if ($params['type'] == NOTIFY_WALL && $params['item']['item_blocked'] == ITEM_MODERATED) {
$seen = 0;
}
}
$e = q(
"select * from notify where otype = '%s' and xname = '%s' and verb = '%s' and link = '%s' and ntype = %d limit 1",
dbesc($datarray['otype']),
dbesc($datarray['xname']),
dbesc($datarray['verb']),
dbesc($datarray['link']),
intval($datarray['ntype'])
);
if ($e) {
logger('duplicated notification');
pop_lang();
return;
}
$r = q(
"insert into notify (hash,xname,url,photo,created,msg,aid,uid,link,parent,seen,ntype,verb,otype)
values('%s','%s','%s','%s','%s','%s',%d,%d,'%s','%s',%d,%d,'%s','%s')",
dbesc($datarray['hash']),
dbesc($datarray['xname']),
dbesc($datarray['url']),
dbesc($datarray['photo']),
dbesc($datarray['created']),
dbesc(''), // will fill this in below after the record is created
intval($datarray['aid']),
intval($datarray['uid']),
dbesc($datarray['link']),
dbesc($datarray['parent']),
intval($seen),
intval($datarray['ntype']),
dbesc($datarray['verb']),
dbesc($datarray['otype'])
);
$r = q(
"select id from notify where hash = '%s' and uid = %d limit 1",
dbesc($hash),
intval($recip['channel_id'])
);
if ($r) {
$notify_id = $r[0]['id'];
} else {
logger('notification not found.');
pop_lang();
return;
}
$itemlink = z_root() . '/notify/view/' . $notify_id;
$msg = str_replace('$itemlink', $itemlink, $epreamble);
// wretched hack, but we don't want to duplicate all the preamble variations and we also don't want to screw up a translation
if ((App::$language === 'en' || (! App::$language)) && strpos($msg, ', ')) {
$msg = substr($msg, strpos($msg, ', ') + 1);
}
$r = q(
"update notify set msg = '%s' where id = %d and uid = %d",
dbesc($msg),
intval($notify_id),
intval($datarray['uid'])
);
// send email notification if notification preferences permit
require_once('bbcode.php');
if ((intval($recip['channel_notifyflags']) & intval($params['type'])) || $params['type'] == NOTIFY_SYSTEM) {
logger('notification: sending notification email');
$hn = get_pconfig($recip['channel_id'], 'system', 'email_notify_host');
if ($hn && (! stristr(App::get_hostname(), $hn))) {
// this isn't the email notification host
pop_lang();
return;
}
$textversion = strip_tags(html_entity_decode(bbcode(stripslashes(str_replace(array("\\r", "\\n"), array( "", "\n"), $body))), ENT_QUOTES, 'UTF-8'));
$htmlversion = bbcode(stripslashes(str_replace(array("\\r","\\n"), array("","<br>\n"), $body)));
// use $_SESSION['zid_override'] to force zid() to use
// the recipient address instead of the current observer
$_SESSION['zid_override'] = Channel::get_webfinger($recip);
$_SESSION['zrl_override'] = z_root() . '/channel/' . $recip['channel_address'];
$textversion = zidify_links($textversion);
$htmlversion = zidify_links($htmlversion);
// unset when done to revert to normal behaviour
unset($_SESSION['zid_override']);
unset($_SESSION['zrl_override']);
$datarray = [];
$datarray['banner'] = $banner;
$datarray['product'] = $product;
$datarray['preamble'] = $preamble;
$datarray['sitename'] = $sitename;
$datarray['siteurl'] = $siteurl;
$datarray['type'] = $params['type'];
$datarray['parent'] = $params['parent_mid'];
$datarray['source_name'] = $sender['xchan_name'];
$datarray['source_link'] = $sender['xchan_url'];
$datarray['source_photo'] = $sender['xchan_photo_s'];
$datarray['uid'] = $recip['channel_id'];
$datarray['username'] = $recip['channel_name'];
$datarray['hsitelink'] = $hsitelink;
$datarray['tsitelink'] = $tsitelink;
$datarray['hitemlink'] = '<a href="' . $itemlink . '">' . $itemlink . '</a>';
$datarray['titemlink'] = $itemlink;
$datarray['thanks'] = $thanks;
$datarray['site_admin'] = $site_admin;
$datarray['opt_out1'] = $opt_out1;
$datarray['opt_out2'] = $opt_out2;
$datarray['hopt_out2'] = $hopt_out2;
$datarray['title'] = stripslashes($title);
$datarray['htmlversion'] = $htmlversion;
$datarray['textversion'] = $textversion;
$datarray['subject'] = $subject;
$datarray['headers'] = $additional_mail_header;
$datarray['email_secure'] = false;
Hook::call('enotify_mail', $datarray);
// Default to private - don't disclose message contents over insecure channels (such as email)
// Might be interesting to use GPG,PGP,S/MIME encryption instead
// but we'll save that for a clever plugin developer to implement
$private_activity = false;
if (! $datarray['email_secure']) {
switch ($params['type']) {
case NOTIFY_WALL:
case NOTIFY_TAGSELF:
case NOTIFY_POKE:
case NOTIFY_RESHARE:
case NOTIFY_COMMENT:
if (! $private) {
break;
}
$private_activity = true;
case NOTIFY_MAIL:
$datarray['textversion'] = $datarray['htmlversion'] = $datarray['title'] = '';
$datarray['subject'] = preg_replace('/' . preg_quote(t('[$Projectname:Notify]'), '/') . '/', '$0*', $datarray['subject']);
break;
default:
break;
}
}
if (
$private_activity
&& intval(get_pconfig($datarray['uid'], 'system', 'ignore_private_notifications'))
) {
pop_lang();
return;
}
// load the template for private message notifications
$tpl = Theme::get_template('email_notify_html.tpl');
$email_html_body = replace_macros($tpl, array(
'$banner' => $datarray['banner'],
'$notify_icon' => System::get_site_icon(),
'$product' => $datarray['product'],
'$preamble' => $salutation . '<br><br>' . $datarray['preamble'],
'$sitename' => $datarray['sitename'],
'$siteurl' => $datarray['siteurl'],
'$source_name' => $datarray['source_name'],
'$source_link' => $datarray['source_link'],
'$source_photo' => $datarray['source_photo'],
'$username' => $datarray['to_name'],
'$hsitelink' => $datarray['hsitelink'],
'$hitemlink' => $datarray['hitemlink'],
'$thanks' => $datarray['thanks'],
'$site_admin' => $datarray['site_admin'],
'$opt_out1' => $datarray['opt_out1'],
'$opt_out2' => $datarray['hopt_out2'],
'$title' => $datarray['title'],
'$htmlversion' => $datarray['htmlversion'],
));
// load the template for private message notifications
$tpl = Theme::get_template('email_notify_text.tpl');
$email_text_body = replace_macros($tpl, array(
'$banner' => $datarray['banner'],
'$product' => $datarray['product'],
'$preamble' => $salutation . "\n\n" . $datarray['preamble'],
'$sitename' => $datarray['sitename'],
'$siteurl' => $datarray['siteurl'],
'$source_name' => $datarray['source_name'],
'$source_link' => $datarray['source_link'],
'$source_photo' => $datarray['source_photo'],
'$username' => $datarray['to_name'],
'$tsitelink' => $datarray['tsitelink'],
'$titemlink' => $datarray['titemlink'],
'$thanks' => $datarray['thanks'],
'$site_admin' => $datarray['site_admin'],
'$opt_out1' => $datarray['opt_out1'],
'$opt_out2' => $datarray['opt_out2'],
'$title' => $datarray['title'],
'$textversion' => $datarray['textversion'],
));
// logger('text: ' . $email_text_body);
// use the EmailNotification library to send the message
$to_email = $recip['account_email'];
$e = get_pconfig($recip['channel_id'], 'system', 'notification_email', false);
if ($e) {
$to_email = $e;
}
$addrs = explode(',', $to_email);
foreach ($addrs as $addr) {
self::send(array(
'fromName' => $sender_name,
'fromEmail' => $sender_email,
'replyTo' => $reply_email,
'toEmail' => $addr,
'messageSubject' => $datarray['subject'],
'htmlVersion' => $email_html_body,
'textVersion' => $email_text_body,
'additionalMailHeader' => $datarray['headers'],
));
}
}
pop_lang();
}
/**
* @brief Send a multipart/alternative message with Text and HTML versions.
*
* @param array $params an assoziative array with:
* * \e string \b fromName name of the sender
* * \e string \b fromEmail email of the sender
* * \e string \b replyTo replyTo address to direct responses
* * \e string \b toEmail destination email address
* * \e string \b messageSubject subject of the message
* * \e string \b htmlVersion html version of the message
* * \e string \b textVersion text only version of the message
* * \e string \b additionalMailHeader additions to the smtp mail header
*/
public static function send($params)
{
$params['sent'] = false;
$params['result'] = false;
Hook::call('email_send', $params);
if ($params['sent']) {
logger("notification: enotify::send (addon) returns " . (($params['result']) ? 'success' : 'failure'), LOGGER_DEBUG);
return $params['result'];
}
$fromName = email_header_encode(html_entity_decode($params['fromName'], ENT_QUOTES, 'UTF-8'), 'UTF-8');
$messageSubject = email_header_encode(html_entity_decode($params['messageSubject'], ENT_QUOTES, 'UTF-8'), 'UTF-8');
// generate a mime boundary
$mimeBoundary = rand(0, 9) . "-"
. rand(100000000, 999999999) . "-"
. rand(100000000, 999999999) . "=:"
. rand(10000, 99999);
// generate a multipart/alternative message header
$messageHeader =
$params['additionalMailHeader'] .
"From: $fromName <{$params['fromEmail']}>" . PHP_EOL .
"Reply-To: $fromName <{$params['replyTo']}>" . PHP_EOL .
"MIME-Version: 1.0" . PHP_EOL .
"Content-Type: multipart/alternative; boundary=\"{$mimeBoundary}\"";
// assemble the final multipart message body with the text and html types included
$textBody = chunk_split(base64_encode($params['textVersion']));
$htmlBody = chunk_split(base64_encode($params['htmlVersion']));
$multipartMessageBody =
"--" . $mimeBoundary . PHP_EOL . // plain text section
"Content-Type: text/plain; charset=UTF-8" . PHP_EOL .
"Content-Transfer-Encoding: base64" . PHP_EOL . PHP_EOL .
$textBody . PHP_EOL .
"--" . $mimeBoundary . PHP_EOL . // text/html section
"Content-Type: text/html; charset=UTF-8" . PHP_EOL .
"Content-Transfer-Encoding: base64" . PHP_EOL . PHP_EOL .
$htmlBody . PHP_EOL .
"--" . $mimeBoundary . "--" . PHP_EOL; // message ending
// send the message
$res = mail(
$params['toEmail'], // send to address
$messageSubject, // subject
$multipartMessageBody, // message body
$messageHeader // message headers
);
logger("notification: enotify::send returns " . (($res) ? 'success' : 'failure'), LOGGER_DEBUG);
return $res;
}
public static function format($item)
{
$ret = '';
$expire = intval(get_config('system', 'default_expire_days'));
$expire_date = (($expire) ? datetime_convert('UTC', 'UTC', 'now - ' . $expire . ' days') : NULL_DATE);
require_once('include/conversation.php');
// Call localize_item to get a one line status for activities.
// This should set $item['localize'] to indicate we have a brief summary.
// and perhaps $item['shortlocalize'] for an even briefer summary
localize_item($item);
if ($item['shortlocalize']) {
$itemem_text = $item['shortlocalize'];
} elseif ($item['localize']) {
$itemem_text = $item['localize'];
} else {
$itemem_text = (($item['item_thread_top'])
? t('created a new post')
: sprintf(t('reacted to %s\'s conversation'), $item['owner']['xchan_name']));
if ($item['verb'] === 'Announce') {
$itemem_text = sprintf(t('shared %s\'s post'), $item['owner']['xchan_name']);
}
}
if ($item['item_private'] == 2) {
$itemem_text = t('sent a direct message');
}
$edit = false;
if ($item['edited'] > $item['created']) {
if ($item['item_thread_top']) {
$itemem_text = sprintf(t('edited a post dated %s'), relative_date($item['created']));
$edit = true;
} else {
$itemem_text = sprintf(t('edited a comment dated %s'), relative_date($item['created']));
$edit = true;
}
}
if (LibBlock::fetch_by_entity(local_channel(), $item['author']['xchan_hash'])) {
return [];
}
// convert this logic into a json array just like the system notifications
$x = array(
'notify_link' => $item['llink'],
'name' => $item['author']['xchan_name'],
'addr' => $item['author']['xchan_addr'],
'url' => $item['author']['xchan_url'],
'photo' => $item['author']['xchan_photo_s'],
'when' => relative_date(($edit) ? $item['edited'] : $item['created']),
'class' => (intval($item['item_unseen']) ? 'notify-unseen' : 'notify-seen'),
'b64mid' => ((in_array($item['verb'], [ACTIVITY_LIKE, ACTIVITY_DISLIKE])) ? gen_link_id($item['thr_parent']) : gen_link_id($item['mid'])),
'notify_id' => 'undefined',
'thread_top' => (($item['item_thread_top']) ? true : false),
'message' => strip_tags(bbcode($itemem_text)),
// these are for the superblock addon
'hash' => $item['author']['xchan_hash'],
'uid' => local_channel(),
'display' => true
);
$post_date = (($edit) ? $item['edited'] : $item['created']);
if ($post_date && $post_date < $expire_date) {
return [];
}
Hook::call('enotify_format', $x);
if (! $x['display']) {
return [];
}
return $x;
}
}

63
Code/Lib/ExtendedZip.php Normal file
View file

@ -0,0 +1,63 @@
<?php
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
namespace Code\Lib;
use ZipArchive;
/**
* Description of ExtendedZip
*
* @author andrew
*/
class ExtendedZip extends ZipArchive
{
// Member function to add a whole file system subtree to the archive
public function addTree($dirname, $localname = '')
{
if ($localname) {
$this->addEmptyDir($localname);
}
$this->_addTree($dirname, $localname);
}
// Internal function, to recurse
protected function _addTree($dirname, $localname)
{
$dir = opendir($dirname);
while ($filename = readdir($dir)) {
// Discard . and ..
if ($filename == '.' || $filename == '..') {
continue;
}
// Proceed according to type
$path = $dirname . '/' . $filename;
$localpath = $localname ? ($localname . '/' . $filename) : $filename;
if (is_dir($path)) {
// Directory: add & recurse
$this->addEmptyDir($localpath);
$this->_addTree($path, $localpath);
} elseif (is_file($path)) {
// File: just add
$this->addFile($path, $localpath);
}
}
closedir($dir);
}
// Helper function
public static function zipTree($dirname, $zipFilename, $flags = 0, $localname = '')
{
$zip = new self();
$zip->open($zipFilename, $flags);
$zip->addTree($dirname, $localname);
$zip->close();
}
}

572
Code/Lib/Features.php Normal file
View file

@ -0,0 +1,572 @@
<?php
namespace Code\Lib;
use App;
use Code\Extend\Hook;
class Features {
public static function enabled($uid, $feature)
{
$x = get_config('feature_lock', $feature);
if ($x === false) {
$x = get_pconfig($uid, 'feature', $feature);
if ($x === false) {
$x = get_config('feature', $feature);
if ($x === false) {
$x = self::get_default($feature);
}
}
}
$arr = array('uid' => $uid, 'feature' => $feature, 'enabled' => $x);
Hook::call('feature_enabled', $arr);
return($arr['enabled']);
}
public static function get_default($feature)
{
$f = Features::get(false);
foreach ($f as $cat) {
foreach ($cat as $feat) {
if (is_array($feat) && $feat[0] === $feature) {
return $feat[3];
}
}
}
return false;
}
public static function level($feature, $def)
{
$x = get_config('feature_level', $feature);
if ($x !== false) {
return intval($x);
}
return $def;
}
public static function get($filtered = true, $level = (-1))
{
$account = App::get_account();
$arr = [
// General
'general' => [
t('General Features'),
[
'start_menu',
t('New Member Links'),
t('Display new member quick links menu'),
(($account && $account['account_created'] > datetime_convert('', '', 'now - 30 days')) ? true : false),
get_config('feature_lock', 'start_menu'),
self::level('start_menu', 1),
],
[
'advanced_profiles',
t('Advanced Profiles'),
t('Additional profile sections and selections'),
false,
get_config('feature_lock', 'advanced_profiles'),
self::level('advanced_profiles', 1),
],
// [
// 'profile_export',
// t('Profile Import/Export'),
// t('Save and load profile details across sites/channels'),
// false,
// get_config('feature_lock','profile_export'),
// self::level('profile_export',3),
// ],
// [
// 'webpages',
// t('Web Pages'),
// t('Provide managed web pages on your channel'),
// false,
// get_config('feature_lock','webpages'),
// self::level('webpages',3),
// ],
// [
// 'wiki',
// t('Wiki'),
// t('Provide a wiki for your channel'),
// false,
// get_config('feature_lock','wiki'),
// self::level('wiki',2),
// ],
/*
[
'hide_rating',
t('Hide Rating'),
t('Hide the rating buttons on your channel and profile pages. Note: People can still rate you somewhere else.'),
false,
get_config('feature_lock','hide_rating'),
self::level('hide_rating',3),
],
*/
[
'private_notes',
t('Private Notes'),
t('Enables a tool to store notes and reminders (note: not encrypted)'),
false,
get_config('feature_lock', 'private_notes'),
self::level('private_notes', 1),
],
// [
// 'cards',
// t('Cards'),
// t('Create personal planning cards'),
// false,
// get_config('feature_lock','cards'),
// self::level('cards',1),
// ],
[
'articles',
t('Articles'),
t('Create interactive articles'),
false,
get_config('feature_lock', 'articles'),
self::level('articles', 1),
],
// [
// 'nav_channel_select',
// t('Navigation Channel Select'),
// t('Change channels directly from within the navigation dropdown menu'),
// false,
// get_config('feature_lock','nav_channel_select'),
// self::level('nav_channel_select',3),
// ],
[
'photo_location',
t('Photo Location'),
t('If location data is available on uploaded photos, link this to a map.'),
false,
get_config('feature_lock', 'photo_location'),
self::level('photo_location', 2),
],
// [
// 'ajaxchat',
// t('Access Controlled Chatrooms'),
// t('Provide chatrooms and chat services with access control.'),
// true,
// get_config('feature_lock','ajaxchat'),
// self::level('ajaxchat',1),
// ],
// [
// 'smart_birthdays',
// t('Smart Birthdays'),
// t('Make birthday events timezone aware in case your friends are scattered across the planet.'),
// true,
// get_config('feature_lock','smart_birthdays'),
// self::level('smart_birthdays',2),
// ],
[
'event_tz_select',
t('Event Timezone Selection'),
t('Allow event creation in timezones other than your own.'),
false,
get_config('feature_lock', 'event_tz_select'),
self::level('event_tz_select', 2),
],
// [
// 'premium_channel',
// t('Premium Channel'),
// t('Allows you to set restrictions and terms on those that connect with your channel'),
// false,
// get_config('feature_lock','premium_channel'),
// self::level('premium_channel',4),
// ],
[
'advanced_dirsearch',
t('Advanced Directory Search'),
t('Allows creation of complex directory search queries'),
false,
get_config('feature_lock', 'advanced_dirsearch'),
self::level('advanced_dirsearch', 4),
],
[
'advanced_theming',
t('Advanced Theme and Layout Settings'),
t('Allows fine tuning of themes and page layouts'),
false,
get_config('feature_lock', 'advanced_theming'),
self::level('advanced_theming', 4),
],
],
'access_control' => [
t('Access Control and Permissions'),
[
'groups',
t('Privacy Groups'),
t('Enable management and selection of privacy groups'),
false,
get_config('feature_lock', 'groups'),
self::level('groups', 0),
],
// [
// 'multi_profiles',
// t('Multiple Profiles'),
// t('Ability to create multiple profiles'),
// false,
// get_config('feature_lock','multi_profiles'),
// self::level('multi_profiles',3),
// ],
[
'permcats',
t('Permission Categories'),
t('Create custom connection permission limits'),
true,
get_config('feature_lock','permcats'),
self::level('permcats',2),
],
// [
// 'oauth_clients',
// t('OAuth1 Clients'),
// t('Manage OAuth1 authenticatication tokens for mobile and remote apps.'),
// false,
// get_config('feature_lock','oauth_clients'),
// self::level('oauth_clients',1),
// ],
[
'oauth2_clients',
t('OAuth2 Clients'),
t('Manage OAuth2 authenticatication tokens for mobile and remote apps.'),
false,
get_config('feature_lock', 'oauth2_clients'),
self::level('oauth2_clients', 1),
],
// [
// 'access_tokens',
// t('Access Tokens'),
// t('Create access tokens so that non-members can access private content.'),
// false,
// get_config('feature_lock','access_tokens'),
// self::level('access_tokens',2),
// ],
],
// Post composition
'composition' => [
t('Post Composition Features'),
// [
// 'large_photos',
// t('Large Photos'),
// t('Include large (1024px) photo thumbnails in posts. If not enabled, use small (640px) photo thumbnails'),
// false,
// get_config('feature_lock','large_photos'),
// self::level('large_photos',1),
// ],
// [
// 'channel_sources',
// t('Channel Sources'),
// t('Automatically import channel content from other channels or feeds'),
// false,
// get_config('feature_lock','channel_sources'),
// self::level('channel_sources',3),
// ],
[
'content_encrypt',
t('Browser Encryption'),
t('Provide optional browser-to-browser encryption of content with a shared secret key'),
true,
get_config('feature_lock', 'content_encrypt'),
self::level('content_encrypt', 3),
],
// [
// 'consensus_tools',
// t('Enable Voting Tools'),
// t('Provide a class of post which others can vote on'),
// false,
// get_config('feature_lock','consensus_tools'),
// self::level('consensus_tools',3),
// ],
// [
// 'disable_comments',
// t('Disable Comments'),
// t('Provide the option to disable comments for a post'),
// false,
// get_config('feature_lock','disable_comments'),
// self::level('disable_comments',2),
// ],
// [
// 'delayed_posting',
// t('Delayed Posting'),
// t('Allow posts to be published at a later date'),
// false,
// get_config('feature_lock','delayed_posting'),
// self::level('delayed_posting',2),
// ],
// [
// 'content_expire',
// t('Content Expiration'),
// t('Remove posts/comments and/or private messages at a future time'),
// false,
// get_config('feature_lock','content_expire'),
// self::level('content_expire',1),
// ],
[
'suppress_duplicates',
t('Suppress Duplicate Posts/Comments'),
t('Prevent posts with identical content to be published with less than two minutes in between submissions.'),
true,
get_config('feature_lock', 'suppress_duplicates'),
self::level('suppress_duplicates', 1),
],
[
'auto_save_draft',
t('Auto-save drafts of posts and comments'),
t('Automatically saves post and comment drafts in local browser storage to help prevent accidental loss of compositions'),
true,
get_config('feature_lock', 'auto_save_draft'),
self::level('auto_save_draft', 1),
],
],
// Network Tools
'net_module' => [
t('Network and Stream Filtering'),
[
'archives',
t('Search by Date'),
t('Ability to select posts by date ranges'),
false,
get_config('feature_lock', 'archives'),
self::level('archives', 1),
],
[
'savedsearch',
t('Saved Searches'),
t('Save search terms for re-use'),
false,
get_config('feature_lock', 'savedsearch'),
self::level('savedsearch', 2),
],
[
'order_tab',
t('Alternate Stream Order'),
t('Ability to order the stream by last post date, last comment date or unthreaded activities'),
false,
get_config('feature_lock', 'order_tab'),
self::level('order_tab', 2),
],
[
'name_tab',
t('Contact Filter'),
t('Ability to display only posts of a selected contact'),
false,
get_config('feature_lock', 'name_tab'),
self::level('name_tab', 1),
],
[
'forums_tab',
t('Forum Filter'),
t('Ability to display only posts of a specific forum'),
false,
get_config('feature_lock', 'forums_tab'),
self::level('forums_tab', 1),
],
[
'personal_tab',
t('Personal Posts Filter'),
t('Ability to display only posts that you\'ve interacted on'),
false,
get_config('feature_lock', 'personal_tab'),
self::level('personal_tab', 1),
],
[
'affinity',
t('Affinity Tool'),
t('Filter stream activity by depth of relationships'),
false,
get_config('feature_lock', 'affinity'),
self::level('affinity', 1),
],
[
'suggest',
t('Suggest Channels'),
t('Show friend and connection suggestions'),
false,
get_config('feature_lock', 'suggest'),
self::level('suggest', 1),
],
[
'connfilter',
t('Connection Filtering'),
t('Filter incoming posts from connections based on keywords/content'),
false,
get_config('feature_lock', 'connfilter'),
self::level('connfilter', 3),
],
],
// Item tools
'tools' => [
t('Post/Comment Tools'),
[
'commtag',
t('Community Tagging'),
t('Ability to tag existing posts'),
false,
get_config('feature_lock', 'commtag'),
self::level('commtag', 1),
],
[
'categories',
t('Post Categories'),
t('Add categories to your posts'),
false,
get_config('feature_lock', 'categories'),
self::level('categories', 1),
],
[
'emojis',
t('Emoji Reactions'),
t('Add emoji reaction ability to posts'),
true,
get_config('feature_lock', 'emojis'),
self::level('emojis', 1),
],
[
'filing',
t('Saved Folders'),
t('Ability to file posts under folders'),
false,
get_config('feature_lock', 'filing'),
self::level('filing', 2),
],
[
'dislike',
t('Dislike Posts'),
t('Ability to dislike posts/comments'),
false,
get_config('feature_lock', 'dislike'),
self::level('dislike', 1),
],
// [
// 'star_posts',
// t('Star Posts'),
// t('Ability to mark special posts with a star indicator'),
// false,
// get_config('feature_lock','star_posts'),
// self::level('star_posts',1),
// ],
//
[
'tagadelic',
t('Tag Cloud'),
t('Provide a personal tag cloud on your channel page'),
false,
get_config('feature_lock', 'tagadelic'),
self::level('tagadelic', 2),
],
],
];
$x = [ 'features' => $arr, ];
Hook::call('get_features', $x);
$arr = $x['features'];
// removed any locked features and remove the entire category if this makes it empty
if ($filtered) {
$narr = [];
foreach ($arr as $k => $x) {
$narr[$k] = [ $arr[$k][0] ];
$has_items = false;
for ($y = 0; $y < count($arr[$k]); $y++) {
$disabled = false;
if (is_array($arr[$k][$y])) {
if ($arr[$k][$y][4] !== false) {
$disabled = true;
}
if (! $disabled) {
$has_items = true;
$narr[$k][$y] = $arr[$k][$y];
}
}
}
if (! $has_items) {
unset($narr[$k]);
}
}
} else {
$narr = $arr;
}
return $narr;
}
}

57
Code/Lib/Hashpath.php Normal file
View file

@ -0,0 +1,57 @@
<?php
namespace Code\Lib;
/*
* Code\Lib\Hashpath
*
* Creates hashed directory structures for fast access and resistance to overloading any single directory with files.
*
* Takes a $url which could be any string
* a $prefix which is where to place the hash directory in the filesystem, default is current directory
* use an empty string for $prefix to place hash directories directly off the root directory
* an optional $depth to indicate the hash level
* $depth = 1, 256 directories, suitable for < 384K records/files
* $depth = 2, 65536 directories, suitable for < 98M records/files
* $depth = 3, 16777216 directories, suitable for < 2.5B records/files
* ...
* The total number of records anticipated divided by the number of hash directories should generally be kept to
* less than 1500 entries for optimum performance though this varies by operating system and filesystem type.
* ext4 uses 32 bit inode numbers (~4B record limit) so use caution or alternative filesystem types with $depth above 3.
* an optional $mkdir (boolean) to recursively create the directory (ignoring errors) before returning
*
* examples: for a $url of 'abcdefg' and prefix of 'path' the following paths are returned for $depth = 1 and $depth = 3
* path/7d/7d1a54127b222502f5b79b5fb0803061152a44f92b37e23c6527baf665d4da9a
* path/7d/1a/54/7d1a54127b222502f5b79b5fb0803061152a44f92b37e23c6527baf665d4da9a
*
* see also: boot.php:os_mkdir() - here we provide the equivalent of mkdir -p with permissions of 770.
*
*/
class Hashpath
{
public static function path($url, $prefix = '.', $depth = 1, $mkdir = true)
{
$hash = hash('sha256', $url);
$start = 0;
$slice = 2;
if ($depth < 1) {
$depth = 1;
}
$sluglen = $depth * $slice;
do {
$slug = substr($hash, $start, $slice);
$prefix .= '/' . $slug;
$start += $slice;
$sluglen -= $slice;
} while ($sluglen);
if ($mkdir) {
os_mkdir($prefix, STORAGE_DEFAULT_PERMISSIONS, true);
}
return $prefix . '/' . $hash;
}
}

208
Code/Lib/Head.php Normal file
View file

@ -0,0 +1,208 @@
<?php
namespace Code\Lib;
use App;
use Code\Render\Theme;
class Head {
/**
* @brief add CSS to \<head\>
*
* @param string $src
* @param string $media change media attribute (default to 'screen')
*/
public static function add_css($src, $media = 'screen')
{
App::$css_sources[] = [ $src, $media ];
}
public static function remove_css($src, $media = 'screen')
{
$index = array_search([$src, $media], App::$css_sources);
if ($index !== false) {
unset(App::$css_sources[$index]);
}
// re-index the array
App::$css_sources = array_values(App::$css_sources);
}
public static function get_css()
{
$str = EMPTY_STR;
$sources = App::$css_sources;
if (is_array($sources) && $sources) {
foreach ($sources as $source) {
$str .= self::format_css_if_exists($source);
}
}
return $str;
}
public static function add_link($arr)
{
if ($arr) {
App::$linkrel[] = $arr;
}
}
public static function get_links()
{
$str = '';
$sources = App::$linkrel;
if (is_array($sources) && $sources) {
foreach ($sources as $source) {
if (is_array($source) && $source) {
$str .= '<link';
foreach ($source as $k => $v) {
$str .= ' ' . $k . '="' . $v . '"';
}
$str .= ' />' . "\r\n";
}
}
}
return $str;
}
public static function format_css_if_exists($source)
{
// script_path() returns https://yoursite.tld
$path_prefix = self::script_path();
$script = $source[0];
if (strpos($script, '/') !== false) {
// The script is a path relative to the server root
$path = $script;
// If the url starts with // then it's an absolute URL
if (substr($script, 0, 2) === '//') {
$path_prefix = '';
}
} else {
// It's a file from the theme
$path = '/' . Theme::include($script);
}
if ($path) {
$qstring = ((parse_url($path, PHP_URL_QUERY)) ? '&' : '?') . 'v=' . STD_VERSION;
return '<link rel="stylesheet" href="' . $path_prefix . $path . $qstring . '" type="text/css" media="' . $source[1] . '">' . "\r\n";
}
}
/**
* This basically calculates the baseurl. We have other functions to do that, but
* there was an issue with script paths and mixed-content whose details are arcane
* and perhaps lost in the message archives. The short answer is that we're ignoring
* the URL which we are "supposed" to use, and generating script paths relative to
* the URL which we are currently using; in order to ensure they are found and aren't
* blocked due to mixed content issues.
*
* @return string
*/
public static function script_path()
{
if (x($_SERVER, 'HTTPS') && $_SERVER['HTTPS']) {
$scheme = 'https';
} elseif (x($_SERVER, 'SERVER_PORT') && (intval($_SERVER['SERVER_PORT']) == 443)) {
$scheme = 'https';
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' || !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on') {
$scheme = 'https';
} else {
$scheme = 'http';
}
// Some proxy setups may require using http_host
if (isset(App::$config['system']['script_path_use_http_host']) && intval(App::$config['system']['script_path_use_http_host'])) {
$server_var = 'HTTP_HOST';
} else {
$server_var = 'SERVER_NAME';
}
if (x($_SERVER, $server_var)) {
$hostname = $_SERVER[$server_var];
} else {
return z_root();
}
return $scheme . '://' . $hostname;
}
public static function add_js($src, $priority = 0)
{
if (! (isset(App::$js_sources[$priority]) && is_array(App::$js_sources[$priority]))) {
App::$js_sources[$priority] = [];
}
App::$js_sources[$priority][] = $src;
}
public static function remove_js($src, $priority = 0)
{
$index = array_search($src, App::$js_sources[$priority]);
if ($index !== false) {
unset(App::$js_sources[$priority][$index]);
}
}
/**
* We should probably try to register main.js with a high priority, but currently
* we handle it separately and put it at the end of the html head block in case
* any other javascript is added outside the head_add_js construct.
*
* @return string
*/
public static function get_js()
{
$str = '';
if (App::$js_sources) {
ksort(App::$js_sources, SORT_NUMERIC);
foreach (App::$js_sources as $sources) {
if (count($sources)) {
foreach ($sources as $source) {
if ($source === 'main.js') {
continue;
}
$str .= self::format_js_if_exists($source);
}
}
}
}
return $str;
}
public static function get_main_js()
{
return self::format_js_if_exists('main.js', true);
}
public static function format_js_if_exists($source)
{
$path_prefix = self::script_path();
if (strpos($source, '/') !== false) {
// The source is a known path on the system
$path = $source;
// If the url starts with // then it's an absolute URL
if (substr($source, 0, 2) === '//') {
$path_prefix = '';
}
} else {
// It's a file from the theme
$path = '/' . Theme::include($source);
}
if ($path) {
$qstring = ((parse_url($path, PHP_URL_QUERY)) ? '&' : '?') . 'v=' . STD_VERSION;
return '<script src="' . $path_prefix . $path . $qstring . '" ></script>' . "\r\n" ;
}
}
}

182
Code/Lib/IConfig.php Normal file
View file

@ -0,0 +1,182 @@
<?php
namespace Code\Lib;
class IConfig
{
public static function Load(&$item)
{
return;
}
public static function Get(&$item, $family, $key, $default = false)
{
$is_item = false;
if (is_array($item)) {
$is_item = true;
if ((! array_key_exists('iconfig', $item)) || (! is_array($item['iconfig']))) {
$item['iconfig'] = [];
}
if (array_key_exists('item_id', $item)) {
$iid = $item['item_id'];
} else {
$iid = ((isset($item['id'])) ? $item['id'] : 0);
}
if (array_key_exists('iconfig', $item) && is_array($item['iconfig'])) {
foreach ($item['iconfig'] as $c) {
if ($c['cat'] == $family && $c['k'] == $key) {
return $c['v'];
}
}
}
} elseif (intval($item)) {
$iid = $item;
}
if (! $iid) {
return $default;
}
$r = q(
"select * from iconfig where iid = %d and cat = '%s' and k = '%s' limit 1",
intval($iid),
dbesc($family),
dbesc($key)
);
if ($r) {
$r[0]['v'] = unserialise($r[0]['v']);
if ($is_item) {
$item['iconfig'][] = $r[0];
}
return $r[0]['v'];
}
return $default;
}
/**
* IConfig::Set(&$item, $family, $key, $value, $sharing = false);
*
* $item - item array or item id. If passed an array the iconfig meta information is
* added to the item structure (which will need to be saved with item_store eventually).
* If passed an id, the DB is updated, but may not be federated and/or cloned.
* $family - namespace of meta variable
* $key - key of meta variable
* $value - value of meta variable
* $sharing - boolean (default false); if true the meta information is propagated with the item
* to other sites/channels, mostly useful when $item is an array and has not yet been stored/delivered.
* If the meta information is added after delivery and you wish it to be shared, it may be necessary to
* alter the item edited timestamp and invoke the delivery process on the updated item. The edited
* timestamp needs to be altered in order to trigger an item_store_update() at the receiving end.
*/
public static function Set(&$item, $family, $key, $value, $sharing = false)
{
$dbvalue = ((is_array($value)) ? serialise($value) : $value);
$dbvalue = ((is_bool($dbvalue)) ? intval($dbvalue) : $dbvalue);
$is_item = false;
$idx = null;
if (is_array($item)) {
$is_item = true;
if ((! array_key_exists('iconfig', $item)) || (! is_array($item['iconfig']))) {
$item['iconfig'] = [];
} elseif ($item['iconfig']) {
for ($x = 0; $x < count($item['iconfig']); $x++) {
if ($item['iconfig'][$x]['cat'] == $family && $item['iconfig'][$x]['k'] == $key) {
$idx = $x;
}
}
}
$entry = array('cat' => $family, 'k' => $key, 'v' => $value, 'sharing' => $sharing);
if (is_null($idx)) {
$item['iconfig'][] = $entry;
} else {
$item['iconfig'][$idx] = $entry;
}
return $value;
}
if (intval($item)) {
$iid = intval($item);
}
if (! $iid) {
return false;
}
if (self::Get($item, $family, $key) === false) {
$r = q(
"insert into iconfig( iid, cat, k, v, sharing ) values ( %d, '%s', '%s', '%s', %d ) ",
intval($iid),
dbesc($family),
dbesc($key),
dbesc($dbvalue),
intval($sharing)
);
} else {
$r = q(
"update iconfig set v = '%s', sharing = %d where iid = %d and cat = '%s' and k = '%s' ",
dbesc($dbvalue),
intval($sharing),
intval($iid),
dbesc($family),
dbesc($key)
);
}
if (! $r) {
return false;
}
return $value;
}
public static function Delete(&$item, $family, $key)
{
$is_item = false;
$idx = null;
if (is_array($item)) {
$is_item = true;
if (is_array($item['iconfig'])) {
for ($x = 0; $x < count($item['iconfig']); $x++) {
if ($item['iconfig'][$x]['cat'] == $family && $item['iconfig'][$x]['k'] == $key) {
unset($item['iconfig'][$x]);
}
}
// re-order the array index
$item['iconfig'] = array_values($item['iconfig']);
}
return true;
}
if (intval($item)) {
$iid = intval($item);
}
if (! $iid) {
return false;
}
return q(
"delete from iconfig where iid = %d and cat = '%s' and k = '%s' ",
intval($iid),
dbesc($family),
dbesc($key)
);
}
}

83
Code/Lib/Img_cache.php Normal file
View file

@ -0,0 +1,83 @@
<?php
namespace Code\Lib;
use Code\Lib\Hashpath;
use Code\Daemon\Run;
class Img_cache
{
public static $cache_life = 18600 * 7;
public static function get_filename($url, $prefix = '.')
{
return Hashpath::path($url, $prefix);
}
// Check to see if we have this url in our cache
// If we have it return true.
// If we do not, or the cache file is empty or expired, return false
// but attempt to fetch the entry in the background
public static function check($url, $prefix = '.')
{
if (strpos($url, z_root()) !== false) {
return false;
}
$path = self::get_filename($url, $prefix);
if (file_exists($path)) {
$t = filemtime($path);
if ($t && time() - $t >= self::$cache_life) {
Run::Summon(['Cache_image', $url, $path]);
return false;
} else {
return ((filesize($path)) ? true : false);
}
}
// Cache_image invokes url_to_cache() as a background task
Run::Summon(['Cache_image', $url, $path]);
return false;
}
public static function url_to_cache($url, $file)
{
$fp = fopen($file, 'wb');
if (!$fp) {
logger('failed to open storage file: ' . $file, LOGGER_NORMAL, LOG_ERR);
return false;
}
// don't check certs, and since we're running in the background,
// allow a two-minute timeout rather than the default one minute.
// This is a compromise. We want to cache all the slow sites we can,
// but don't want to rack up too many processes doing so.
$redirects = 0;
$x = z_fetch_url($url, true, $redirects, ['filep' => $fp, 'novalidate' => true, 'timeout' => 120]);
fclose($fp);
if ($x['success'] && file_exists($file)) {
$i = @getimagesize($file);
if ($i && $i[2]) { // looking for non-zero imagetype
Run::Summon(['CacheThumb', basename($file)]);
return true;
}
}
// We could not cache the image for some reason. Leave an empty file here
// to provide a record of the attempt. We'll use this as a flag to avoid
// doing it again repeatedly.
file_put_contents($file, EMPTY_STR);
logger('cache failed from ' . $url);
return false;
}
}

127
Code/Lib/Img_filesize.php Normal file
View file

@ -0,0 +1,127 @@
<?php
namespace Code\Lib;
class Img_filesize
{
private $url;
public function __construct($url)
{
$this->url = $url;
}
public function getSize()
{
$size = null;
if (stripos($this->url, z_root() . '/photo') !== false) {
$size = self::getLocalFileSize($this->url);
}
if (!$size) {
$size = getRemoteFileSize($this->url);
}
return $size;
}
public static function getLocalFileSize($url)
{
$fname = basename($url);
$resolution = 0;
if (strpos($fname, '.') !== false) {
$fname = substr($fname, 0, strpos($fname, '.'));
}
if (substr($fname, -2, 1) == '-') {
$resolution = intval(substr($fname, -1, 1));
$fname = substr($fname, 0, -2);
}
$r = q(
"SELECT filesize FROM photo WHERE resource_id = '%s' AND imgscale = %d LIMIT 1",
dbesc($fname),
intval($resolution)
);
if ($r) {
return $r[0]['filesize'];
}
return null;
}
}
/**
* Try to determine the size of a remote file by making an HTTP request for
* a byte range, or look for the content-length header in the response.
* The function aborts the transfer as soon as the size is found, or if no
* length headers are returned, it aborts the transfer.
*
* @return int|null null if size could not be determined, or length of content
*/
function getRemoteFileSize($url)
{
$ch = curl_init($url);
$headers = array(
'Range: bytes=0-1',
'Connection: close',
);
$in_headers = true;
$size = null;
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2450.0 Iron/46.0.2450.0');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_VERBOSE, 0); // set to 1 to debug
curl_setopt($ch, CURLOPT_STDERR, fopen('php://output', 'r'));
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $line) use (&$in_headers, &$size) {
$length = strlen($line);
if (trim($line) == '') {
$in_headers = false;
}
list($header, $content) = explode(':', $line, 2);
$header = strtolower(trim($header));
if ($header == 'content-range') {
// found a content-range header
list($rng, $s) = explode('/', $content, 2);
$size = (int)$s;
return 0; // aborts transfer
} elseif ($header == 'content-length' && 206 != curl_getinfo($curl, CURLINFO_HTTP_CODE)) {
// found content-length header and this is not a 206 Partial Content response (range response)
$size = (int)$content;
return 0;
} else {
// continue
return $length;
}
});
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($curl, $data) use ($in_headers) {
if (!$in_headers) {
// shouldn't be here unless we couldn't determine file size
// abort transfer
return 0;
}
// write function is also called when reading headers
return strlen($data);
});
curl_exec($ch);
curl_getinfo($ch);
curl_close($ch);
return $size;
}

76
Code/Lib/Infocon.php Normal file
View file

@ -0,0 +1,76 @@
<?php
namespace Code\Lib;
/**
* Infocon class: extract information and configuration structures from source modules.
*/
use Exception;
use Symfony\Component\Yaml\Yaml;
class Infocon {
public static function from_file($name) {
$info = NULL;
if (file_exists($name)) {
try {
$info = Yaml::parseFile($name, Yaml::PARSE_DATETIME);
}
catch (Exception $e) {
;
}
}
return $info;
}
public static function from_str($str) {
$info = NULL;
if ($str) {
try {
$info = Yaml::parse($str, Yaml::PARSE_DATETIME);
}
catch (Exception $e) {
;
}
}
return $info;
}
public static function from_c_comment($file) {
$info = NULL;
try {
$code = file_get_contents($file);
}
catch (Exception $e) {
;
}
// Match and fetch the first C-style comment
$result = preg_match("|/\*.*\*/|msU", $code, $matches);
if ($result) {
$lines = explode("\n", $m[0]);
foreach ($lines as $line) {
$line = trim($line, "\t\n\r */");
if ($line != "") {
list($k, $v) = array_map("trim", explode(":", $line, 2));
$k = strtolower($k);
// multiple lines with the same key are turned into an array
if (isset($info[$k])) {
if (is_array($info[$k])) {
$info[$k][] = $v;
}
else {
$info[$k] = [ $info[$k], $v ];
}
}
else {
$info[$k] = $v;
}
}
}
}
return $info;
}
}

73
Code/Lib/JSalmon.php Normal file
View file

@ -0,0 +1,73 @@
<?php
namespace Code\Lib;
use Code\Web\HTTPSig;
class JSalmon
{
public static function sign($data, $key_id, $key, $data_type = 'application/x-nomad+json')
{
$data = base64url_encode(json_encode($data, true), true); // strip padding
$encoding = 'base64url';
$algorithm = 'RSA-SHA256';
$data = preg_replace('/\s+/', '', $data);
// precomputed base64url encoding of data_type, encoding, algorithm concatenated with periods
$precomputed = '.' . base64url_encode($data_type, true) . '.YmFzZTY0dXJs.UlNBLVNIQTI1Ng';
$signature = base64url_encode(Crypto::sign($data . $precomputed, $key), true);
return ([
'signed' => true,
'data' => $data,
'data_type' => $data_type,
'encoding' => $encoding,
'alg' => $algorithm,
'sigs' => [
'value' => $signature,
'key_id' => base64url_encode($key_id, true)
]
]);
}
public static function verify($x)
{
logger('verify');
$ret = ['results' => []];
if (!is_array($x)) {
return false;
}
if (!(array_key_exists('signed', $x) && $x['signed'])) {
return false;
}
$signed_data = preg_replace('/\s+/', '', $x['data']) . '.'
. base64url_encode($x['data_type'], true) . '.'
. base64url_encode($x['encoding'], true) . '.'
. base64url_encode($x['alg'], true);
$key = HTTPSig::get_key(EMPTY_STR, 'zot6', base64url_decode($x['sigs']['key_id']));
logger('key: ' . print_r($key, true));
if ($key['portable_id'] && $key['public_key']) {
if (Crypto::verify($signed_data, base64url_decode($x['sigs']['value']), $key['public_key'])) {
logger('verified');
$ret = ['success' => true, 'signer' => $key['portable_id'], 'hubloc' => $key['hubloc']];
}
}
return $ret;
}
public static function unpack($data)
{
return json_decode(base64url_decode($data), true);
}
}

102
Code/Lib/Keyutils.php Normal file
View file

@ -0,0 +1,102 @@
<?php
namespace Code\Lib;
use phpseclib\Crypt\RSA;
use phpseclib\Math\BigInteger;
/**
* Keyutils
* Convert RSA keys between various formats
*/
class Keyutils
{
/**
* @param string $m modulo
* @param string $e exponent
* @return string
*/
public static function meToPem($m, $e)
{
$rsa = new RSA();
$rsa->loadKey([
'e' => new BigInteger($e, 256),
'n' => new BigInteger($m, 256)
]);
return $rsa->getPublicKey();
}
/**
* @param string key
* @return string
*/
public static function rsaToPem($key)
{
$rsa = new RSA();
$rsa->setPublicKey($key);
return $rsa->getPublicKey(RSA::PUBLIC_FORMAT_PKCS8);
}
/**
* @param string key
* @return string
*/
public static function pemToRsa($key)
{
$rsa = new RSA();
$rsa->setPublicKey($key);
return $rsa->getPublicKey(RSA::PUBLIC_FORMAT_PKCS1);
}
/**
* @param string $key key
* @param string $m reference modulo
* @param string $e reference exponent
*/
public static function pemToMe($key, &$m, &$e)
{
$rsa = new RSA();
$rsa->loadKey($key);
$rsa->setPublicKey();
$m = $rsa->modulus->toBytes();
$e = $rsa->exponent->toBytes();
}
/**
* @param string $pubkey
* @return string
*/
public static function salmonKey($pubkey)
{
self::pemToMe($pubkey, $m, $e);
return 'RSA' . '.' . base64url_encode($m, true) . '.' . base64url_encode($e, true);
}
/**
* @param string $key
* @return string
*/
public static function convertSalmonKey($key)
{
if (strstr($key, ',')) {
$rawkey = substr($key, strpos($key, ',') + 1);
} else {
$rawkey = substr($key, 5);
}
$key_info = explode('.', $rawkey);
$m = base64url_decode($key_info[1]);
$e = base64url_decode($key_info[2]);
return self::meToPem($m, $e);
}
}

139
Code/Lib/LDSignatures.php Normal file
View file

@ -0,0 +1,139 @@
<?php
namespace Code\Lib;
use Exception;
use Code\Lib\Activity;
use Code\Lib\Channel;
require_once('library/jsonld/jsonld.php');
class LDSignatures
{
public static function verify($data, $pubkey)
{
$ohash = self::hash(self::signable_options($data['signature']));
$dhash = self::hash(self::signable_data($data));
$x = Crypto::verify($ohash . $dhash, base64_decode($data['signature']['signatureValue']), $pubkey);
logger('LD-verify: ' . intval($x));
return $x;
}
public static function dopplesign(&$data, $channel)
{
// remove for the time being - performance issues
// $data['magicEnv'] = self::salmon_sign($data,$channel);
return self::sign($data, $channel);
}
public static function sign($data, $channel)
{
$options = [
'type' => 'RsaSignature2017',
'nonce' => random_string(64),
'creator' => Channel::url($channel),
'created' => datetime_convert('UTC', 'UTC', 'now', 'Y-m-d\TH:i:s\Z')
];
$ohash = self::hash(self::signable_options($options));
$dhash = self::hash(self::signable_data($data));
$options['signatureValue'] = base64_encode(Crypto::sign($ohash . $dhash, $channel['channel_prvkey']));
return $options;
}
public static function signable_data($data)
{
$newdata = [];
if ($data) {
foreach ($data as $k => $v) {
if (!in_array($k, ['signature'])) {
$newdata[$k] = $v;
}
}
}
return json_encode($newdata, JSON_UNESCAPED_SLASHES);
}
public static function signable_options($options)
{
$newopts = ['@context' => 'https://w3id.org/identity/v1'];
if ($options) {
foreach ($options as $k => $v) {
if (!in_array($k, ['type', 'id', 'signatureValue'])) {
$newopts[$k] = $v;
}
}
}
return json_encode($newopts, JSON_UNESCAPED_SLASHES);
}
public static function hash($obj)
{
return hash('sha256', self::normalise($obj));
}
public static function normalise($data)
{
if (is_string($data)) {
$data = json_decode($data);
}
if (!is_object($data)) {
return '';
}
jsonld_set_document_loader('jsonld_document_loader');
try {
$d = jsonld_normalize($data, ['algorithm' => 'URDNA2015', 'format' => 'application/nquads']);
} catch (Exception $e) {
// Don't log the exception - this can exhaust memory
// logger('normalise error:' . print_r($e,true));
logger('normalise error: ' . print_r($data, true));
}
return $d;
}
public static function salmon_sign($data, $channel)
{
$arr = $data;
$data = json_encode($data, JSON_UNESCAPED_SLASHES);
$data = base64url_encode($data, false); // do not strip padding
$data_type = 'application/activity+json';
$encoding = 'base64url';
$algorithm = 'RSA-SHA256';
$keyhash = base64url_encode(Channel::url($channel));
$data = str_replace(array(" ", "\t", "\r", "\n"), array("", "", "", ""), $data);
// precomputed base64url encoding of data_type, encoding, algorithm concatenated with periods
$precomputed = '.' . base64url_encode($data_type, false) . '.YmFzZTY0dXJs.UlNBLVNIQTI1Ng==';
$signature = base64url_encode(Crypto::sign($data . $precomputed, $channel['channel_prvkey']));
return ([
'id' => $arr['id'],
'meData' => $data,
'meDataType' => $data_type,
'meEncoding' => $encoding,
'meAlgorithm' => $algorithm,
'meCreator' => Channel::url($channel),
'meSignatureValue' => $signature
]);
}
}

118
Code/Lib/LibBlock.php Normal file
View file

@ -0,0 +1,118 @@
<?php
namespace Code\Lib;
class LibBlock
{
public static $cache = [];
public static $empty = [];
// This limits the number of DB queries for fetch_by_entity to once per page load.
public static function fetch_from_cache($channel_id, $entity)
{
if (!isset(self::$cache[$channel_id])) {
if (!isset(self::$empty[$channel_id])) {
self::$cache[$channel_id] = self::fetch($channel_id);
if (!self::$cache[$channel_id]) {
self::$empty[$channel_id] = true;
}
}
}
if (isset(self::$cache[$channel_id]) && self::$cache[$channel_id] && is_array(self::$cache[$channel_id])) {
foreach (self::$cache[$channel_id] as $entry) {
if (is_array($entry) && strcasecmp($entry['block_entity'], $entity) === 0) {
return $entry;
}
}
}
return false;
}
public static function store($arr)
{
$arr['block_entity'] = trim($arr['block_entity']);
if (!$arr['block_entity']) {
return false;
}
$arr['block_channel_id'] = ((array_key_exists('block_channel_id', $arr)) ? intval($arr['block_channel_id']) : 0);
$arr['block_type'] = ((array_key_exists('block_type', $arr)) ? intval($arr['block_type']) : BLOCKTYPE_CHANNEL);
$arr['block_comment'] = ((array_key_exists('block_comment', $arr)) ? escape_tags(trim($arr['block_comment'])) : EMPTY_STR);
if (!intval($arr['block_id'])) {
$r = q(
"select * from block where block_channel_id = %d and block_entity = '%s' and block_type = %d limit 1",
intval($arr['block_channel_id']),
dbesc($arr['block_entity']),
intval($arr['block_type'])
);
if ($r) {
$arr['block_id'] = $r[0]['block_id'];
}
}
if (intval($arr['block_id'])) {
return q(
"UPDATE block set block_channel_id = %d, block_entity = '%s', block_type = %d, block_comment = '%s' where block_id = %d",
intval($arr['block_channel_id']),
dbesc($arr['block_entity']),
intval($arr['block_type']),
dbesc($arr['block_comment']),
intval($arr['block_id'])
);
} else {
return create_table_from_array('block', $arr);
}
}
public static function remove($channel_id, $entity)
{
return q(
"delete from block where block_channel_id = %d and block_entity = '%s'",
intval($channel_id),
dbesc($entity)
);
}
public static function fetch_by_id($channel_id, $id)
{
if (!intval($channel_id)) {
return false;
}
$r = q(
"select * from block where block_channel_id = %d and block_id = %d ",
intval($channel_id)
);
return (($r) ? array_shift($r) : $r);
}
public static function fetch_by_entity($channel_id, $entity)
{
if (!intval($channel_id)) {
return false;
}
return self::fetch_from_cache($channel_id, $entity);
}
public static function fetch($channel_id, $type = false)
{
if (!intval($channel_id)) {
return [];
}
$sql_extra = (($type === false) ? EMPTY_STR : " and block_type = " . intval($type));
$r = q(
"select * from block where block_channel_id = %d $sql_extra",
intval($channel_id)
);
return $r;
}
}

192
Code/Lib/Libacl.php Normal file
View file

@ -0,0 +1,192 @@
<?php
namespace Code\Lib;
use App;
use Code\Lib\Apps;
use Code\Lib\PermissionDescription;
use Code\Render\Theme;
class Libacl
{
public static function fixacl(&$item)
{
$item = str_replace([ '<', '>' ], [ '', '' ], $item);
}
/**
* Builds a modal dialog for editing permissions, using acl_selector.tpl as the template.
*
* @param array $defaults Optional access control list for the initial state of the dialog.
* @param bool $show_jotnets Whether plugins for federated networks should be included in the permissions dialog
* @param PermissionDescription $emptyACL_description - An optional description for the permission implied by selecting an empty ACL. Preferably an instance of PermissionDescription.
* @param string $dialog_description Optional message to include at the top of the dialog. E.g. "Warning: Post permissions cannot be changed once sent".
* @param string $context_help Allows the dialog to present a help icon. E.g. "acl_dialog_post"
* @param bool $readonly Not implemented yet. When implemented, the dialog will use acl_readonly.tpl instead, so that permissions may be viewed for posts that can no longer have their permissions changed.
*
* @return string html modal dialog built from acl_selector.tpl
*/
public static function populate($defaults = null, $show_jotnets = true, $emptyACL_description = '', $dialog_description = '', $context_help = '', $readonly = false)
{
$allow_cid = $allow_gid = $deny_cid = $deny_gid = false;
$showall_origin = '';
$showall_icon = 'fa-globe';
$role = get_pconfig(local_channel(), 'system', 'permissions_role');
if (! $emptyACL_description) {
$showall_caption = t('Visible to your default audience');
} elseif (is_a($emptyACL_description, '\\Code\\Lib\\PermissionDescription')) {
$showall_caption = $emptyACL_description->get_permission_description();
$showall_origin = (($role === 'custom') ? $emptyACL_description->get_permission_origin_description() : '');
$showall_icon = $emptyACL_description->get_permission_icon();
} else {
// For backwards compatibility we still accept a string... for now!
$showall_caption = $emptyACL_description;
}
if (is_array($defaults)) {
$allow_cid = ((strlen($defaults['allow_cid']))
? explode('><', $defaults['allow_cid']) : [] );
$allow_gid = ((strlen($defaults['allow_gid']))
? explode('><', $defaults['allow_gid']) : [] );
$deny_cid = ((strlen($defaults['deny_cid']))
? explode('><', $defaults['deny_cid']) : [] );
$deny_gid = ((strlen($defaults['deny_gid']))
? explode('><', $defaults['deny_gid']) : [] );
array_walk($allow_cid, ['\\Code\\Lib\\Libacl', 'fixacl']);
array_walk($allow_gid, ['\\Code\\Lib\\Libacl', 'fixacl']);
array_walk($deny_cid, ['\\Code\\Lib\\Libacl','fixacl']);
array_walk($deny_gid, ['\\Code\\Lib\\Libacl','fixacl']);
}
$channel = ((local_channel()) ? App::get_channel() : '');
$has_acl = false;
$single_group = false;
$just_me = false;
$custom = false;
if ($allow_cid || $allow_gid || $deny_gid || $deny_cid) {
$has_acl = true;
$custom = true;
}
if (count($allow_gid) === 1 && (! $allow_cid) && (! $deny_gid) && (! $deny_cid)) {
$single_group = true;
$custom = false;
}
if (count($allow_cid) === 1 && $channel && $allow_cid[0] === $channel['channel_hash'] && (! $allow_gid) && (! $deny_gid) && (! $deny_cid)) {
$just_me = true;
$custom = false;
}
$groups = EMPTY_STR;
$r = q(
"SELECT id, hash, gname FROM pgrp WHERE deleted = 0 AND uid = %d ORDER BY gname ASC",
intval(local_channel())
);
if ($r) {
foreach ($r as $rr) {
$selected = (($single_group && $rr['hash'] === $allow_gid[0]) ? ' selected = "selected" ' : '');
$groups .= '<option id="' . $rr['id'] . '" value="' . $rr['hash'] . '"' . $selected . '>' . $rr['gname'] . ' ' . t('(List)') . '</option>' . "\r\n";
}
}
if ($channel && Apps::system_app_installed($channel['channel_id'], 'Virtual Lists')) {
$selected = (($single_group && 'connections:' . $channel['channel_hash'] === $allow_gid[0]) ? ' selected = "selected" ' : '');
$groups .= '<option id="vg1" value="connections:' . $channel['channel_hash'] . '"' . $selected . '>' . t('My connections') . ' ' . t('(Virtual List)') . '</option>' . "\r\n";
if (get_pconfig($channel['channel_id'], 'system', 'activitypub', get_config('system', 'activitypub', ACTIVITYPUB_ENABLED))) {
$selected = (($single_group && 'activitypub:' . $channel['channel_hash'] === $allow_gid[0]) ? ' selected = "selected" ' : '');
$groups .= '<option id="vg2" value="activitypub:' . $channel['channel_hash'] . '"' . $selected . '>' . t('My ActivityPub connections') . ' ' . t('(Virtual List)') . '</option>' . "\r\n";
}
$selected = (($single_group && 'zot:' . $channel['channel_hash'] === $allow_gid[0]) ? ' selected = "selected" ' : '');
$groups .= '<option id="vg3" value="zot:' . $channel['channel_hash'] . '"' . $selected . '>' . t('My Nomad connections') . ' ' . t('(Virtual List)') . '</option>' . "\r\n";
}
$forums = get_forum_channels(local_channel(), 1);
$selected = false;
if ($forums) {
foreach ($forums as $f) {
$selected = (($single_group && $f['hash'] === $allow_cid[0]) ? ' selected = "selected" ' : '');
$groups .= '<option id="^' . $f['abook_id'] . '" value="^' . $f['xchan_hash'] . '"' . $selected . '>' . $f['xchan_name'] . ' ' . t('(Group)') . '</option>' . "\r\n";
}
}
// preset acl with DM to a single xchan (not a group)
if ($selected === false && count($allow_cid) === 1 && $channel && $allow_cid[0] !== $channel['channel_hash'] && (! $allow_gid) && (! $deny_gid) && (! $deny_cid)) {
$f = q(
"select * from xchan where xchan_hash = '%s'",
dbesc($allow_cid[0])
);
if ($f) {
$custom = false;
$selected = ' selected="selected" ';
$groups .= '<option id="^DM" value="^' . $f[0]['xchan_hash'] . '"' . $selected . '>' . $f[0]['xchan_name'] . ' ' . t('(DM)') . '</option>' . "\r\n";
}
}
$tpl = Theme::get_template("acl_selector.tpl");
$o = replace_macros($tpl, array(
'$showall' => $showall_caption,
'$onlyme' => t('Only me'),
'$groups' => $groups,
'$public_selected' => (($has_acl) ? false : ' selected="selected" '),
'$justme_selected' => (($just_me) ? ' selected="selected" ' : ''),
'$custom_selected' => (($custom) ? ' selected="selected" ' : ''),
'$showallOrigin' => $showall_origin,
'$showallIcon' => $showall_icon,
'$select_label' => t('Who can see this?'),
'$custom' => t('Custom selection'),
'$showlimitedDesc' => t('Select "Show" to allow viewing. "Don\'t show" lets you override and limit the scope of "Show".'),
'$show' => t('Show'),
'$hide' => t("Don't show"),
'$search' => t('Search'),
'$allowcid' => json_encode($allow_cid),
'$allowgid' => json_encode($allow_gid),
'$denycid' => json_encode($deny_cid),
'$denygid' => json_encode($deny_gid),
'$aclModalTitle' => t('Permissions'),
'$aclModalDesc' => $dialog_description,
'$aclModalDismiss' => t('Close'),
// '$helpUrl' => (($context_help == '') ? '' : (z_root() . '/help/' . $context_help))
));
return $o;
}
/**
* Returns a string that's suitable for passing as the $dialog_description argument to a
* populate() call for wall posts or network posts.
*
* This string is needed in 3 different files, and our .po translation system currently
* cannot be used as a string table (because the value is always the key in english) so
* I've centralized the value here (making this function name the "key") until we have a
* better way.
*
* @return string Description to present to user in modal permissions dialog
*/
public static function get_post_aclDialogDescription()
{
// I'm trying to make two points in this description text - warn about finality of wall
// post permissions, and try to clear up confusion that these permissions set who is
// *shown* the post, istead of who is able to see the post, i.e. make it clear that clicking
// the "Show" button on a group does not post it to the feed of people in that group, it
// mearly allows those people to view the post if they are viewing/following this channel.
$description = t('Post permissions cannot be changed after a post is shared.<br>These permissions set who is allowed to view the post.');
// Lets keep the emphasis styling seperate from the translation. It may change.
//$emphasisOpen = '<b><a href="' . z_root() . '/help/acl_dialog_post" target="hubzilla-help">';
//$emphasisClose = '</a></b>';
return $description;
}
}

697
Code/Lib/Libprofile.php Normal file
View file

@ -0,0 +1,697 @@
<?php
namespace Code\Lib;
use App;
use Code\Lib\Channel;
use Code\Lib\Features;
use Code\Lib\Menu;
use Code\Extend\Hook;
use Code\Render\Theme;
class Libprofile
{
/**
* @brief Loads a profile into the App structure.
*
* The function requires the nickname of a valid channel.
*
* Permissions of the current observer are checked. If a restricted profile is available
* to the current observer, that will be loaded instead of the channel default profile.
*
* The channel owner can set $profile to a valid profile_guid to preview that profile.
*
* The channel default theme is also selected for use, unless over-riden elsewhere.
*
* @param string $nickname
* @param string $profile_guid
*/
public static function load($nickname, $profile = '')
{
// logger('Libprofile::load: ' . $nickname . (($profile) ? ' profile: ' . $profile : ''));
$channel = Channel::from_username($nickname);
if (!$channel) {
logger('profile error: ' . App::$query_string, LOGGER_DEBUG);
notice(t('Requested channel is not available.') . EOL);
App::$error = 404;
return;
}
// get the current observer
$observer = App::get_observer();
$can_view_profile = true;
// Can the observer see our profile?
require_once('include/permissions.php');
if (!perm_is_allowed($channel['channel_id'], (($observer) ? $observer['xchan_hash'] : ''), 'view_profile')) {
$can_view_profile = false;
}
if (!$profile) {
$r = q(
"SELECT abook_profile FROM abook WHERE abook_xchan = '%s' and abook_channel = '%d' limit 1",
dbesc(($observer) ? $observer['xchan_hash'] : ''),
intval($channel['channel_id'])
);
if ($r) {
$profile = $r[0]['abook_profile'];
}
}
$p = null;
if ($profile) {
$p = q(
"SELECT profile.uid AS profile_uid, profile.*, channel.* FROM profile
LEFT JOIN channel ON profile.uid = channel.channel_id
WHERE channel.channel_address = '%s' AND profile.profile_guid = '%s' LIMIT 1",
dbesc($nickname),
dbesc($profile)
);
if (!$p) {
$p = q(
"SELECT profile.uid AS profile_uid, profile.*, channel.* FROM profile
LEFT JOIN channel ON profile.uid = channel.channel_id
WHERE channel.channel_address = '%s' AND profile.id = %d LIMIT 1",
dbesc($nickname),
intval($profile)
);
}
}
if (!$p) {
$p = q(
"SELECT profile.uid AS profile_uid, profile.*, channel.* FROM profile
LEFT JOIN channel ON profile.uid = channel.channel_id
WHERE channel.channel_address = '%s' and channel_removed = 0
AND profile.is_default = 1 LIMIT 1",
dbesc($nickname)
);
}
if (!$p) {
logger('profile error: ' . App::$query_string, LOGGER_DEBUG);
notice(t('Requested profile is not available.') . EOL);
App::$error = 404;
return;
}
$q = q(
"select * from profext where hash = '%s' and channel_id = %d",
dbesc($p[0]['profile_guid']),
intval($p[0]['profile_uid'])
);
if ($q) {
$extra_fields = [];
$profile_fields_basic = Channel::get_profile_fields_basic();
$profile_fields_advanced = Channel::get_profile_fields_advanced();
$advanced = ((Features::enabled(local_channel(), 'advanced_profiles')) ? true : false);
if ($advanced) {
$fields = $profile_fields_advanced;
} else {
$fields = $profile_fields_basic;
}
foreach ($q as $qq) {
foreach ($fields as $k => $f) {
if ($k == $qq['k']) {
$p[0][$k] = $qq['v'];
$extra_fields[] = $k;
break;
}
}
}
}
$p[0]['extra_fields'] = ((isset($extra_fields)) ? $extra_fields : []);
$z = q(
"select xchan_photo_date, xchan_addr from xchan where xchan_hash = '%s' limit 1",
dbesc($p[0]['channel_hash'])
);
if ($z) {
$p[0]['picdate'] = $z[0]['xchan_photo_date'];
$p[0]['reddress'] = str_replace('@', '&#x40;', unpunify($z[0]['xchan_addr']));
}
// fetch user tags if this isn't the default profile
if (!$p[0]['is_default']) {
$x = q(
"select keywords from profile where uid = %d and is_default = 1 limit 1",
intval($p[0]['profile_uid'])
);
if ($x && $can_view_profile) {
$p[0]['keywords'] = $x[0]['keywords'];
}
}
if ($p[0]['keywords']) {
$keywords = str_replace(array('#', ',', ' ', ',,'), array('', ' ', ',', ','), $p[0]['keywords']);
if (strlen($keywords) && $can_view_profile) {
if (!isset(App::$page['htmlhead'])) {
App::$page['htmlhead'] = EMPTY_STR;
}
App::$page['htmlhead'] .= '<meta name="keywords" content="' . htmlentities($keywords, ENT_COMPAT, 'UTF-8') . '" />' . "\r\n";
}
}
App::$profile = $p[0];
App::$profile_uid = $p[0]['profile_uid'];
App::$page['title'] = App::$profile['channel_name'] . " - " . unpunify(Channel::get_webfinger(App::$profile));
App::$profile['permission_to_view'] = $can_view_profile;
if ($can_view_profile) {
$online = Channel::get_online_status($nickname);
App::$profile['online_status'] = $online['result'];
}
if (local_channel()) {
App::$profile['channel_mobile_theme'] = get_pconfig(local_channel(), 'system', 'mobile_theme');
$_SESSION['mobile_theme'] = App::$profile['channel_mobile_theme'];
}
/*
* load/reload current theme info
*/
// $_SESSION['theme'] = $p[0]['channel_theme'];
}
public static function edit_menu($uid)
{
$ret = [];
$is_owner = (($uid == local_channel()) ? true : false);
// show edit profile to profile owner
if ($is_owner) {
$ret['menu'] = array(
'chg_photo' => t('Change profile photo'),
'entries' => [],
);
$multi_profiles = Features::enabled(local_channel(), 'multi_profiles');
if ($multi_profiles) {
$ret['multi'] = 1;
$ret['edit'] = [z_root() . '/profiles', t('Edit Profiles'), '', t('Edit')];
$ret['menu']['cr_new'] = t('Create New Profile');
} else {
$ret['edit'] = [z_root() . '/profiles/' . $uid, t('Edit Profile'), '', t('Edit')];
}
$r = q(
"SELECT * FROM profile WHERE uid = %d",
local_channel()
);
if ($r) {
foreach ($r as $rr) {
if (!($multi_profiles || $rr['is_default'])) {
continue;
}
$ret['menu']['entries'][] = [
'photo' => $rr['thumb'],
'id' => $rr['id'],
'alt' => t('Profile Image'),
'profile_name' => $rr['profile_name'],
'isdefault' => $rr['is_default'],
'visible_to_everybody' => t('Visible to everybody'),
'edit_visibility' => t('Edit visibility'),
];
}
}
}
return $ret;
}
/**
* @brief Formats a profile for display in the sidebar.
*
* It is very difficult to templatise the HTML completely
* because of all the conditional logic.
*
* @param array $profile
* @param int $block
* @param bool $show_connect (optional) default true
* @param mixed $zcard (optional) default false
*
* @return HTML string suitable for sidebar inclusion
* Exceptions: Returns empty string if passed $profile is wrong type or not populated
*/
public static function widget($profile, $block = 0, $show_connect = true, $zcard = false)
{
$observer = App::get_observer();
$o = '';
$location = false;
$pdesc = true;
$reddress = true;
if (!perm_is_allowed($profile['uid'], ((is_array($observer)) ? $observer['xchan_hash'] : ''), 'view_profile')) {
$block = true;
}
if ((!is_array($profile)) && (!count($profile))) {
return $o;
}
head_set_icon($profile['thumb']);
if (Channel::is_system($profile['uid'])) {
$show_connect = false;
}
$profile['picdate'] = urlencode($profile['picdate']);
/**
* @hooks profile_sidebar_enter
* Called before generating the 'channel sidebar' or mini-profile.
*/
Hook::call('profile_sidebar_enter', $profile);
$profdm = EMPTY_STR;
$profdm_url = EMPTY_STR;
$can_dm = perm_is_allowed($profile['uid'], (is_array($observer)) ? $observer['xchan_hash'] : EMPTY_STR, 'post_mail') && intval($observer['xchan_type']) !== XCHAN_TYPE_GROUP ;
if (intval($profile['uid']) === local_channel()) {
$can_dm = false;
}
if ($can_dm) {
$dm_path = Libzot::get_rpost_path($observer);
if ($dm_path) {
$profdm = t('Direct Message');
$profdm_url = $dm_path
. '&to='
. urlencode($profile['channel_hash'])
. '&body='
. urlencode('@!{' . $profile['channel_address'] . '@' . App::get_hostname() . '}');
}
}
if ($show_connect) {
// This will return an empty string if we're already connected.
$connect_url = rconnect_url($profile['uid'], get_observer_hash());
$connect = (($connect_url) ? t('Connect') : '');
if ($connect_url) {
$connect_url = sprintf($connect_url, urlencode(Channel::get_webfinger($profile)));
}
// premium channel - over-ride
if ($profile['channel_pageflags'] & PAGE_PREMIUM) {
$connect_url = z_root() . '/connect/' . $profile['channel_address'];
}
}
if (
(x($profile, 'address') == 1)
|| (x($profile, 'locality') == 1)
|| (x($profile, 'region') == 1)
|| (x($profile, 'postal_code') == 1)
|| (x($profile, 'country_name') == 1)
) {
$location = t('Location:');
}
$profile['homepage'] = linkify($profile['homepage'], true);
$gender = ((x($profile, 'gender') == 1) ? t('Gender:') : false);
$marital = ((x($profile, 'marital') == 1) ? t('Status:') : false);
$homepage = ((x($profile, 'homepage') == 1) ? t('Homepage:') : false);
$pronouns = ((x($profile, 'pronouns') == 1) ? t('Pronouns:') : false);
// zap/osada do not have a realtime chat system at this time so don't show online state
// $profile['online'] = (($profile['online_status'] === 'online') ? t('Online Now') : False);
// logger('online: ' . $profile['online']);
$profile['online'] = false;
if (($profile['hidewall'] && (!local_channel()) && (!remote_channel())) || $block) {
$location = $reddress = $pdesc = $gender = $marital = $homepage = false;
}
if ($profile['gender']) {
$profile['gender_icon'] = self::gender_icon($profile['gender']);
}
if ($profile['pronouns']) {
$profile['pronouns_icon'] = self::pronouns_icon($profile['pronouns']);
}
$firstname = ((strpos($profile['channel_name'], ' '))
? trim(substr($profile['channel_name'], 0, strpos($profile['channel_name'], ' '))) : $profile['channel_name']);
$lastname = (($firstname === $profile['channel_name']) ? '' : trim(substr($profile['channel_name'], strlen($firstname))));
$contact_block = contact_block();
$channel_menu = false;
$menu = get_pconfig($profile['uid'], 'system', 'channel_menu');
if ($menu && !$block) {
$m = Menu::fetch($menu, $profile['uid'], $observer['xchan_hash']);
if ($m) {
$channel_menu = Menu::render($m);
}
}
$menublock = get_pconfig($profile['uid'], 'system', 'channel_menublock');
if ($menublock && (!$block)) {
$comanche = new Comanche();
$channel_menu .= $comanche->block($menublock);
}
if ($zcard) {
$tpl = Theme::get_template('profile_vcard_short.tpl');
} else {
$tpl = Theme::get_template('profile_vcard.tpl');
}
$o .= replace_macros($tpl, array(
'$zcard' => $zcard,
'$profile' => $profile,
'$connect' => $connect,
'$connect_url' => $connect_url,
'$profdm' => $profdm,
'$profdm_url' => $profdm_url,
'$location' => $location,
'$gender' => $gender,
'$pronouns' => $pronouns,
'$pdesc' => $pdesc,
'$marital' => $marital,
'$homepage' => $homepage,
'$chanmenu' => $channel_menu,
'$reddress' => $reddress,
'$active' => t('Active'),
'$activewhen' => relative_date($profile['channel_lastpost']),
'$rating' => '',
'$contact_block' => $contact_block,
'$change_photo' => t('Change your profile photo'),
'$copyto' => t('Copy to clipboard'),
'$copied' => t('Address copied to clipboard'),
'$editmenu' => self::edit_menu($profile['uid'])
));
$arr = [
'profile' => $profile,
'entry' => $o
];
/**
* @hooks profile_sidebar
* Called when generating the 'channel sidebar' or mini-profile.
* * \e array \b profile
* * \e string \b entry - The parsed HTML template
*/
Hook::call('profile_sidebar', $arr);
return $arr['entry'];
}
public static function gender_icon($gender)
{
// logger('gender: ' . $gender);
// This can easily get throw off if the observer language is different
// than the channel owner language.
if (strpos(strtolower($gender), strtolower(t('Female'))) !== false) {
return 'venus';
}
if (strpos(strtolower($gender), strtolower(t('Male'))) !== false) {
return 'mars';
}
if (strpos(strtolower($gender), strtolower(t('Trans'))) !== false) {
return 'transgender';
}
if (strpos(strtolower($gender), strtolower(t('Inter'))) !== false) {
return 'transgender';
}
if (strpos(strtolower($gender), strtolower(t('Neuter'))) !== false) {
return 'neuter';
}
if (strpos(strtolower($gender), strtolower(t('Non-specific'))) !== false) {
return 'genderless';
}
return '';
}
public static function pronouns_icon($pronouns)
{
// This can easily get throw off if the observer language is different
// than the channel owner language.
if (strpos(strtolower($pronouns), strtolower(t('She'))) !== false) {
return 'venus';
}
if (strpos(strtolower($pronouns), strtolower(t('Him'))) !== false) {
return 'mars';
}
if (strpos(strtolower($pronouns), strtolower(t('Them'))) !== false) {
return 'users';
}
return '';
}
public static function advanced()
{
if (!perm_is_allowed(App::$profile['profile_uid'], get_observer_hash(), 'view_profile')) {
return '';
}
if (App::$profile['fullname']) {
$profile_fields_basic = Channel::get_profile_fields_basic();
$profile_fields_advanced = Channel::get_profile_fields_advanced();
$advanced = ((Features::enabled(App::$profile['profile_uid'], 'advanced_profiles')) ? true : false);
if ($advanced) {
$fields = $profile_fields_advanced;
} else {
$fields = $profile_fields_basic;
}
$clean_fields = [];
if ($fields) {
foreach ($fields as $k => $v) {
$clean_fields[] = trim($k);
}
}
$tpl = Theme::get_template('profile_advanced.tpl');
$profile = [];
$profile['fullname'] = array(t('Full Name:'), App::$profile['fullname']);
if (App::$profile['gender']) {
$profile['gender'] = array(t('Gender:'), App::$profile['gender']);
}
$ob_hash = get_observer_hash();
// this may not work at all any more, but definitely won't work correctly if the liked profile belongs to a group
// comment out until we are able to look at it much closer
// if($ob_hash && perm_is_allowed(App::$profile['profile_uid'],$ob_hash,'post_like')) {
// $profile['canlike'] = true;
// $profile['likethis'] = t('Like this channel');
// $profile['profile_guid'] = App::$profile['profile_guid'];
// }
// $likers = q("select liker, xchan.* from likes left join xchan on liker = xchan_hash where channel_id = %d and target_type = '%s' and verb = '%s'",
// intval(App::$profile['profile_uid']),
// dbesc(ACTIVITY_OBJ_PROFILE),
// dbesc(ACTIVITY_LIKE)
// );
// $profile['likers'] = [];
// $profile['like_count'] = count($likers);
// $profile['like_button_label'] = tt('Like','Likes',$profile['like_count'],'noun');
// if($likers) {
// foreach($likers as $l)
// $profile['likers'][] = array('name' => $l['xchan_name'],'photo' => zid($l['xchan_photo_s']), 'url' => zid($l['xchan_url']));
// }
if ((App::$profile['dob']) && (App::$profile['dob'] != '0000-00-00')) {
$val = '';
if ((substr(App::$profile['dob'], 5, 2) === '00') || (substr(App::$profile['dob'], 8, 2) === '00')) {
$val = substr(App::$profile['dob'], 0, 4);
}
$year_bd_format = t('j F, Y');
$short_bd_format = t('j F');
if (!$val) {
$val = ((intval(App::$profile['dob']))
? day_translate(datetime_convert('UTC', 'UTC', App::$profile['dob'] . ' 00:00 +00:00', $year_bd_format))
: day_translate(datetime_convert('UTC', 'UTC', '2001-' . substr(App::$profile['dob'], 5) . ' 00:00 +00:00', $short_bd_format)));
}
$profile['birthday'] = array(t('Birthday:'), $val);
}
if ($age = age(App::$profile['dob'], App::$profile['timezone'], '')) {
$profile['age'] = array(t('Age:'), $age);
}
if (App::$profile['marital']) {
$profile['marital'] = array(t('Status:'), App::$profile['marital']);
}
if (App::$profile['partner']) {
$profile['marital']['partner'] = zidify_links(bbcode(App::$profile['partner']));
}
if (strlen(App::$profile['howlong']) && App::$profile['howlong'] > NULL_DATE) {
$profile['howlong'] = relative_date(App::$profile['howlong'], t('for %1$d %2$s'));
}
if (App::$profile['keywords']) {
$keywords = str_replace(',', ' ', App::$profile['keywords']);
$keywords = str_replace(' ', ' ', $keywords);
$karr = explode(' ', $keywords);
if ($karr) {
for ($cnt = 0; $cnt < count($karr); $cnt++) {
$karr[$cnt] = '<a href="' . z_root() . '/directory/f=&keywords=' . trim($karr[$cnt]) . '">' . $karr[$cnt] . '</a>';
}
}
$profile['keywords'] = array(t('Tags:'), implode(' ', $karr));
}
if (App::$profile['sexual']) {
$profile['sexual'] = array(t('Sexual Preference:'), App::$profile['sexual']);
}
if (App::$profile['pronouns']) {
$profile['pronouns'] = array(t('Pronouns:'), App::$profile['pronouns']);
}
if (App::$profile['homepage']) {
$profile['homepage'] = array(t('Homepage:'), linkify(App::$profile['homepage']));
}
if (App::$profile['hometown']) {
$profile['hometown'] = array(t('Hometown:'), linkify(App::$profile['hometown']));
}
if (App::$profile['politic']) {
$profile['politic'] = array(t('Political Views:'), App::$profile['politic']);
}
if (App::$profile['religion']) {
$profile['religion'] = array(t('Religion:'), App::$profile['religion']);
}
if ($txt = prepare_text(App::$profile['about'])) {
$profile['about'] = array(t('About:'), $txt);
}
if ($txt = prepare_text(App::$profile['interest'])) {
$profile['interest'] = array(t('Hobbies/Interests:'), $txt);
}
if ($txt = prepare_text(App::$profile['likes'])) {
$profile['likes'] = array(t('Likes:'), $txt);
}
if ($txt = prepare_text(App::$profile['dislikes'])) {
$profile['dislikes'] = array(t('Dislikes:'), $txt);
}
if ($txt = prepare_text(App::$profile['contact'])) {
$profile['contact'] = array(t('Contact information and Social Networks:'), $txt);
}
if ($txt = prepare_text(App::$profile['channels'])) {
$profile['channels'] = array(t('My other channels:'), $txt);
}
if ($txt = prepare_text(App::$profile['music'])) {
$profile['music'] = array(t('Musical interests:'), $txt);
}
if ($txt = prepare_text(App::$profile['book'])) {
$profile['book'] = array(t('Books, literature:'), $txt);
}
if ($txt = prepare_text(App::$profile['tv'])) {
$profile['tv'] = array(t('Television:'), $txt);
}
if ($txt = prepare_text(App::$profile['film'])) {
$profile['film'] = array(t('Film/dance/culture/entertainment:'), $txt);
}
if ($txt = prepare_text(App::$profile['romance'])) {
$profile['romance'] = array(t('Love/Romance:'), $txt);
}
if ($txt = prepare_text(App::$profile['employment'])) {
$profile['employment'] = array(t('Work/employment:'), $txt);
}
if ($txt = prepare_text(App::$profile['education'])) {
$profile['education'] = array(t('School/education:'), $txt);
}
if (App::$profile['extra_fields']) {
foreach (App::$profile['extra_fields'] as $f) {
$x = q(
"select * from profdef where field_name = '%s' limit 1",
dbesc($f)
);
if ($x && $txt = prepare_text(App::$profile[$f])) {
$profile[$f] = array($x[0]['field_desc'] . ':', $txt);
}
}
$profile['extra_fields'] = App::$profile['extra_fields'];
}
$things = get_things(App::$profile['profile_guid'], App::$profile['profile_uid']);
// logger('mod_profile: things: ' . print_r($things,true), LOGGER_DATA);
// $exportlink = ((App::$profile['profile_vcard']) ? zid(z_root() . '/profile/' . App::$profile['channel_address'] . '/vcard') : '');
return replace_macros($tpl, array(
'$title' => t('Profile'),
'$canlike' => (($profile['canlike']) ? true : false),
'$likethis' => t('Like this thing'),
'$export' => t('Export'),
'$exportlink' => '', // $exportlink,
'$profile' => $profile,
'$fields' => $clean_fields,
'$editmenu' => self::edit_menu(App::$profile['profile_uid']),
'$things' => $things
));
}
return '';
}
}

1303
Code/Lib/Libsync.php Normal file

File diff suppressed because it is too large Load diff

3645
Code/Lib/Libzot.php Normal file

File diff suppressed because it is too large Load diff

543
Code/Lib/Libzotdir.php Normal file
View file

@ -0,0 +1,543 @@
<?php
namespace Code\Lib;
use App;
use Code\Lib\Libzot;
use Code\Lib\Webfinger;
use Code\Lib\Zotfinger;
use Code\Lib\Channel;
use Code\Extend\Hook;
use Code\Render\Theme;
require_once('include/permissions.php');
class Libzotdir
{
/**
* Directories may come and go over time. We will need to check that our
* directory server is still valid occasionally, and reset to something that
* is if our directory has gone offline for any reason
*/
public static function check_upstream_directory()
{
$directory = get_config('system', 'directory_server');
// it's possible there is no directory server configured and the local hub is being used.
// If so, default to preserving the absence of a specific server setting.
$isadir = true;
if ($directory) {
$j = Zotfinger::exec($directory);
if (array_path_exists('data/directory_mode', $j)) {
if ($j['data']['directory_mode'] === 'normal') {
$isadir = false;
}
}
}
if (!$isadir) {
set_config('system', 'directory_server', '');
}
}
public static function get_directory_setting($observer, $setting)
{
if ($observer) {
$ret = get_xconfig($observer, 'directory', $setting);
} else {
$ret = ((array_key_exists($setting, $_SESSION)) ? intval($_SESSION[$setting]) : false);
}
if ($ret === false) {
$ret = get_config('directory', $setting);
if ($ret === false) {
$ret = (in_array($setting, ['globaldir', 'safemode', 'activedir']) ? 1 : 0);
}
}
if ($setting === 'globaldir' && intval(get_config('system', 'localdir_hide'))) {
$ret = 1;
}
return $ret;
}
/**
* @brief Called by the directory_sort widget.
*/
public static function dir_sort_links()
{
$safe_mode = 1;
$observer = get_observer_hash();
$safe_mode = self::get_directory_setting($observer, 'safemode');
$globaldir = self::get_directory_setting($observer, 'globaldir');
$pubforums = self::get_directory_setting($observer, 'chantype');
$activedir = self::get_directory_setting($observer, 'activedir');
$hide_local = intval(get_config('system', 'localdir_hide'));
if ($hide_local) {
$globaldir = 1;
}
// Build urls without order and pubforums so it's easy to tack on the changed value
// Probably there's an easier way to do this
$directory_sort_order = get_config('system', 'directory_sort_order');
if (!$directory_sort_order) {
$directory_sort_order = 'date';
}
$current_order = (($_REQUEST['order']) ? $_REQUEST['order'] : $directory_sort_order);
$suggest = (($_REQUEST['suggest']) ? '&suggest=' . $_REQUEST['suggest'] : '');
$url = 'directory?f=';
$tmp = array_merge($_GET, $_POST);
unset($tmp['suggest']);
unset($tmp['pubforums']);
unset($tmp['type']);
unset($tmp['global']);
unset($tmp['safe']);
unset($tmp['active']);
unset($tmp['req']);
unset($tmp['f']);
$q = http_build_query($tmp);
$forumsurl = $url . (($q) ? '&' . $q : '') . $suggest;
$o = replace_macros(Theme::get_template('dir_sort_links.tpl'), [
'$header' => t('Directory Options'),
'$forumsurl' => $forumsurl,
'$safemode' => array('safemode', t('Safe Mode'), $safe_mode, '', array(t('No'), t('Yes')), ' onchange=\'window.location.href="' . $forumsurl . '&safe="+(this.checked ? 1 : 0)\''),
'$pubforums' => array('pubforums', t('Groups Only'), (($pubforums == 1) ? true : false), '', array(t('No'), t('Yes')), ' onchange=\'window.location.href="' . $forumsurl . '&type="+(this.checked ? 1 : 0)\''),
// '$collections' => array('collections', t('Collections Only'),(($pubforums == 2) ? true : false),'',array(t('No'), t('Yes')),' onchange=\'window.location.href="' . $forumsurl . '&type="+(this.checked ? 2 : 0)\''),
'$hide_local' => $hide_local,
'$globaldir' => array('globaldir', t('This Website Only'), 1 - intval($globaldir), '', array(t('No'), t('Yes')), ' onchange=\'window.location.href="' . $forumsurl . '&global="+(this.checked ? 0 : 1)\''),
'$activedir' => array('activedir', t('Recently Updated'), intval($activedir), '', array(t('No'), t('Yes')), ' onchange=\'window.location.href="' . $forumsurl . '&active="+(this.checked ? 1 : 0)\''),
]);
return $o;
}
/**
* @brief
*
* Given an update record, probe the channel, grab a zot-info packet and refresh/sync the data.
*
* Ignore updating records marked as deleted.
*
* If successful, sets ud_last in the DB to the current datetime for this
* reddress/webbie.
*
* @param array $ud Entry from update table
*/
public static function update_directory_entry($ud)
{
logger('update_directory_entry: ' . print_r($ud, true), LOGGER_DATA);
if ($ud['ud_addr'] && (!($ud['ud_flags'] & UPDATE_FLAGS_DELETED))) {
$success = false;
$href = Webfinger::zot_url(punify($ud['ud_addr']));
if ($href) {
$zf = Zotfinger::exec($href);
}
if (is_array($zf) && array_path_exists('signature/signer', $zf) && $zf['signature']['signer'] === $href && intval($zf['signature']['header_valid'])) {
$xc = Libzot::import_xchan($zf['data'], 0, $ud);
} else {
q(
"update updates set ud_last = '%s' where ud_addr = '%s'",
dbesc(datetime_convert()),
dbesc($ud['ud_addr'])
);
}
}
}
/**
* @brief Push local channel updates to a local directory server.
*
* This is called from Code/Daemon/Directory.php if a profile is to be pushed to the
* directory and the local hub in this case is any kind of directory server.
*
* @param int $uid
* @param bool $force
*/
public static function local_dir_update($uid, $force)
{
logger('local_dir_update: uid: ' . $uid, LOGGER_DEBUG);
$p = q(
"select channel_hash, channel_address, channel_timezone, profile.* from profile left join channel on channel_id = uid where uid = %d and is_default = 1",
intval($uid)
);
$profile = [];
$profile['encoding'] = 'zot';
if ($p) {
$hash = $p[0]['channel_hash'];
$profile['description'] = $p[0]['pdesc'];
$profile['birthday'] = $p[0]['dob'];
if ($age = age($p[0]['dob'], $p[0]['channel_timezone'], '')) {
$profile['age'] = $age;
}
$profile['gender'] = $p[0]['gender'];
$profile['marital'] = $p[0]['marital'];
$profile['sexual'] = $p[0]['sexual'];
$profile['locale'] = $p[0]['locality'];
$profile['region'] = $p[0]['region'];
$profile['postcode'] = $p[0]['postal_code'];
$profile['country'] = $p[0]['country_name'];
$profile['about'] = $p[0]['about'];
$profile['homepage'] = $p[0]['homepage'];
$profile['hometown'] = $p[0]['hometown'];
if ($p[0]['keywords']) {
$tags = [];
$k = explode(' ', $p[0]['keywords']);
if ($k) {
foreach ($k as $kk) {
if (trim($kk)) {
$tags[] = trim($kk);
}
}
}
if ($tags) {
$profile['keywords'] = $tags;
}
}
$hidden = (1 - intval($p[0]['publish']));
// logger('hidden: ' . $hidden);
$r = q(
"select xchan_hidden from xchan where xchan_hash = '%s' limit 1",
dbesc($p[0]['channel_hash'])
);
if (intval($r[0]['xchan_hidden']) != $hidden) {
$r = q(
"update xchan set xchan_hidden = %d where xchan_hash = '%s'",
intval($hidden),
dbesc($p[0]['channel_hash'])
);
}
$arr = ['channel_id' => $uid, 'hash' => $hash, 'profile' => $profile];
Hook::call('local_dir_update', $arr);
$address = Channel::get_webfinger($p[0]);
if (perm_is_allowed($uid, '', 'view_profile')) {
self::import_directory_profile($hash, $arr['profile'], $address, 0);
} else {
// they may have made it private
$r = q(
"delete from xprof where xprof_hash = '%s'",
dbesc($hash)
);
$r = q(
"delete from xtag where xtag_hash = '%s'",
dbesc($hash)
);
}
}
$ud_hash = random_string() . '@' . App::get_hostname();
self::update_modtime($hash, $ud_hash, Channel::get_webfinger($p[0]), (($force) ? UPDATE_FLAGS_FORCED : UPDATE_FLAGS_UPDATED));
}
/**
* @brief Imports a directory profile.
*
* @param string $hash
* @param array $profile
* @param string $addr
* @param number $ud_flags (optional) UPDATE_FLAGS_UPDATED
* @param number $suppress_update (optional) default 0
* @return bool $updated if something changed
*/
public static function import_directory_profile($hash, $profile, $addr, $ud_flags = UPDATE_FLAGS_UPDATED, $suppress_update = 0)
{
logger('import_directory_profile', LOGGER_DEBUG);
if (!$hash) {
return false;
}
$maxlen = get_max_import_size();
if ($maxlen && mb_strlen($profile['about']) > $maxlen) {
$profile['about'] = mb_substr($profile['about'], 0, $maxlen, 'UTF-8');
}
$arr = [];
$arr['xprof_hash'] = $hash;
if (isset($profile['birthday'])) {
$arr['xprof_dob'] = (($profile['birthday'] === '0000-00-00')
? $profile['birthday']
: datetime_convert('', '', $profile['birthday'], 'Y-m-d')); // !!!! check this for 0000 year
}
$arr['xprof_age'] = (isset($profile['age']) ? intval($profile['age']) : 0);
$arr['xprof_desc'] = ((isset($profile['description']) && $profile['description']) ? htmlspecialchars($profile['description'], ENT_COMPAT, 'UTF-8', false) : '');
$arr['xprof_gender'] = ((isset($profile['gender']) && $profile['gender']) ? htmlspecialchars($profile['gender'], ENT_COMPAT, 'UTF-8', false) : '');
$arr['xprof_marital'] = ((isset($profile['marital']) && $profile['marital']) ? htmlspecialchars($profile['marital'], ENT_COMPAT, 'UTF-8', false) : '');
$arr['xprof_sexual'] = ((isset($profile['sexual']) && $profile['sexual']) ? htmlspecialchars($profile['sexual'], ENT_COMPAT, 'UTF-8', false) : '');
$arr['xprof_locale'] = ((isset($profile['locale']) && $profile['locale']) ? htmlspecialchars($profile['locale'], ENT_COMPAT, 'UTF-8', false) : '');
$arr['xprof_region'] = ((isset($profile['region']) && $profile['region']) ? htmlspecialchars($profile['region'], ENT_COMPAT, 'UTF-8', false) : '');
$arr['xprof_postcode'] = ((isset($profile['postcode']) && $profile['postcode']) ? htmlspecialchars($profile['postcode'], ENT_COMPAT, 'UTF-8', false) : '');
$arr['xprof_country'] = ((isset($profile['country']) && $profile['country']) ? htmlspecialchars($profile['country'], ENT_COMPAT, 'UTF-8', false) : '');
$arr['xprof_about'] = ((isset($profile['about']) && $profile['about']) ? htmlspecialchars($profile['about'], ENT_COMPAT, 'UTF-8', false) : '');
$arr['xprof_pronouns'] = ((isset($profile['pronouns']) && $profile['pronouns']) ? htmlspecialchars($profile['pronouns'], ENT_COMPAT, 'UTF-8', false) : '');
$arr['xprof_homepage'] = ((isset($profile['homepage']) && $profile['homepage']) ? htmlspecialchars($profile['homepage'], ENT_COMPAT, 'UTF-8', false) : '');
$arr['xprof_hometown'] = ((isset($profile['hometown']) && $profile['hometown']) ? htmlspecialchars($profile['hometown'], ENT_COMPAT, 'UTF-8', false) : '');
$clean = [];
if (array_key_exists('keywords', $profile) and is_array($profile['keywords'])) {
self::import_directory_keywords($hash, $profile['keywords']);
foreach ($profile['keywords'] as $kw) {
$kw = trim(htmlspecialchars($kw, ENT_COMPAT, 'UTF-8', false));
$kw = trim($kw, ',');
$clean[] = $kw;
}
}
$arr['xprof_keywords'] = implode(' ', $clean);
// Self censored, make it so
// These are not translated, so the German "erwachsenen" keyword will not censor the directory profile. Only the English form - "adult".
if (in_arrayi('nsfw', $clean) || in_arrayi('adult', $clean)) {
q(
"update xchan set xchan_selfcensored = 1 where xchan_hash = '%s'",
dbesc($hash)
);
}
$r = q(
"select * from xprof where xprof_hash = '%s' limit 1",
dbesc($hash)
);
if ($arr['xprof_age'] > 150) {
$arr['xprof_age'] = 150;
}
if ($arr['xprof_age'] < 0) {
$arr['xprof_age'] = 0;
}
if ($r) {
$update = false;
foreach ($r[0] as $k => $v) {
if ((array_key_exists($k, $arr)) && ($arr[$k] != $v)) {
logger('import_directory_profile: update ' . $k . ' => ' . $arr[$k]);
$update = true;
break;
}
}
if ($update) {
q(
"update xprof set
xprof_desc = '%s',
xprof_dob = '%s',
xprof_age = %d,
xprof_gender = '%s',
xprof_marital = '%s',
xprof_sexual = '%s',
xprof_locale = '%s',
xprof_region = '%s',
xprof_postcode = '%s',
xprof_country = '%s',
xprof_about = '%s',
xprof_homepage = '%s',
xprof_hometown = '%s',
xprof_keywords = '%s',
xprof_pronouns = '%s'
where xprof_hash = '%s'",
dbesc($arr['xprof_desc']),
dbesc($arr['xprof_dob']),
intval($arr['xprof_age']),
dbesc($arr['xprof_gender']),
dbesc($arr['xprof_marital']),
dbesc($arr['xprof_sexual']),
dbesc($arr['xprof_locale']),
dbesc($arr['xprof_region']),
dbesc($arr['xprof_postcode']),
dbesc($arr['xprof_country']),
dbesc($arr['xprof_about']),
dbesc($arr['xprof_homepage']),
dbesc($arr['xprof_hometown']),
dbesc($arr['xprof_keywords']),
dbesc($arr['xprof_pronouns']),
dbesc($arr['xprof_hash'])
);
}
} else {
$update = true;
logger('New profile');
q(
"insert into xprof (xprof_hash, xprof_desc, xprof_dob, xprof_age, xprof_gender, xprof_marital, xprof_sexual, xprof_locale, xprof_region, xprof_postcode, xprof_country, xprof_about, xprof_homepage, xprof_hometown, xprof_keywords, xprof_pronouns) values ('%s', '%s', '%s', %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s') ",
dbesc($arr['xprof_hash']),
dbesc($arr['xprof_desc']),
dbesc($arr['xprof_dob']),
intval($arr['xprof_age']),
dbesc($arr['xprof_gender']),
dbesc($arr['xprof_marital']),
dbesc($arr['xprof_sexual']),
dbesc($arr['xprof_locale']),
dbesc($arr['xprof_region']),
dbesc($arr['xprof_postcode']),
dbesc($arr['xprof_country']),
dbesc($arr['xprof_about']),
dbesc($arr['xprof_homepage']),
dbesc($arr['xprof_hometown']),
dbesc($arr['xprof_keywords']),
dbesc($arr['xprof_pronouns'])
);
}
$d = [
'xprof' => $arr,
'profile' => $profile,
'update' => $update
];
/**
* @hooks import_directory_profile
* Called when processing delivery of a profile structure from an external source (usually for directory storage).
* * \e array \b xprof
* * \e array \b profile
* * \e boolean \b update
*/
Hook::call('import_directory_profile', $d);
if (($d['update']) && (!$suppress_update)) {
self::update_modtime($arr['xprof_hash'], new_uuid(), $addr, $ud_flags);
}
q(
"update xchan set xchan_updated = '%s' where xchan_hash = '%s'",
dbesc(datetime_convert()),
dbesc($arr['xprof_hash'])
);
return $d['update'];
}
/**
* @brief
*
* @param string $hash An xtag_hash
* @param array $keywords
*/
public static function import_directory_keywords($hash, $keywords)
{
$existing = [];
$r = q(
"select * from xtag where xtag_hash = '%s' and xtag_flags = 0",
dbesc($hash)
);
if ($r) {
foreach ($r as $rr) {
$existing[] = $rr['xtag_term'];
}
}
$clean = [];
foreach ($keywords as $kw) {
$kw = trim(htmlspecialchars($kw, ENT_COMPAT, 'UTF-8', false));
$kw = trim($kw, ',');
$clean[] = $kw;
}
foreach ($existing as $x) {
if (!in_array($x, $clean)) {
$r = q(
"delete from xtag where xtag_hash = '%s' and xtag_term = '%s' and xtag_flags = 0",
dbesc($hash),
dbesc($x)
);
}
}
foreach ($clean as $x) {
if (!in_array($x, $existing)) {
$r = q(
"insert into xtag ( xtag_hash, xtag_term, xtag_flags) values ( '%s' ,'%s', 0 )",
dbesc($hash),
dbesc($x)
);
}
}
}
/**
* @brief
*
* @param string $hash
* @param string $guid
* @param string $addr
* @param int $flags (optional) default 0
*/
public static function update_modtime($hash, $guid, $addr, $flags = 0)
{
$dirmode = intval(get_config('system', 'directory_mode'));
if ($dirmode == DIRECTORY_MODE_NORMAL) {
return;
}
if ($flags) {
q(
"insert into updates (ud_hash, ud_guid, ud_date, ud_flags, ud_addr ) values ( '%s', '%s', '%s', %d, '%s' )",
dbesc($hash),
dbesc($guid),
dbesc(datetime_convert()),
intval($flags),
dbesc($addr)
);
} else {
q(
"update updates set ud_flags = ( ud_flags | %d ) where ud_addr = '%s' and (ud_flags & %d) = 0 ",
intval(UPDATE_FLAGS_UPDATED),
dbesc($addr),
intval(UPDATE_FLAGS_UPDATED)
);
}
}
}

414
Code/Lib/Markdown.php Normal file
View file

@ -0,0 +1,414 @@
<?php
namespace Code\Lib;
/**
* @brief Some functions for BB and markdown conversions
*/
use Michelf\MarkdownExtra;
use League\HTMLToMarkdown\HtmlConverter;
use League\HTMLToMarkdown\Environment;
use League\HTMLToMarkdown\Converter\ConverterInterface;
use League\HTMLToMarkdown\ElementInterface;
use Code\Extend\Hook;
require_once("include/event.php");
require_once("include/html2bbcode.php");
require_once("include/bbcode.php");
class Markdown
{
/**
* @brief Convert Markdown to bbcode.
*
* We don't want to support a bbcode specific markdown interpreter
* and the markdown library we have is pretty good, but provides HTML output.
* So we'll use that to convert to HTML, then convert the HTML back to bbcode,
* and then clean up a few Diaspora specific constructs.
*
* @param string $s The message as Markdown
* @param bool $use_zrl default false
* @param array $options default empty
* @return string The message converted to bbcode
*/
public static function to_bbcode($s, $use_zrl = false, $options = [])
{
if (is_array($s)) {
btlogger('markdown_to_bb called with array. ' . print_r($s, true), LOGGER_NORMAL, LOG_WARNING);
return '';
}
$s = str_replace("&#xD;", "\r", $s);
$s = str_replace("&#xD;\n&gt;", "", $s);
$s = html_entity_decode($s, ENT_COMPAT, 'UTF-8');
// if empty link text replace with the url
$s = preg_replace("/\[\]\((.*?)\)/ism", '[$1]($1)', $s);
$x = [
'text' => $s,
'zrl' => $use_zrl,
'options' => $options
];
/**
* @hooks markdown_to_bb_init
* * \e string \b text - The message as Markdown and what will get returned
* * \e boolean \b zrl
* * \e array \b options
*/
Hook::call('markdown_to_bb_init', $x);
$s = $x['text'];
// Escaping the hash tags
$s = preg_replace('/\#([^\s\#])/', '&#35;$1', $s);
$s = MarkdownExtra::defaultTransform($s);
if ($options && $options['preserve_lf']) {
$s = str_replace(["\r", "\n"], ["", '<br>'], $s);
} else {
$s = str_replace("\r", "", $s);
}
$s = str_replace('&#35;', '#', $s);
$s = html2bbcode($s);
// Convert everything that looks like a link to a link
if ($use_zrl) {
if (strpos($s, '[/img]') !== false) {
$s = preg_replace_callback("/\[img\](.*?)\[\/img\]/ism", ['\\Code\\Lib\\Markdown', 'use_zrl_cb_img'], $s);
$s = preg_replace_callback("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", ['\\Code\\Lib\\Markdown', 'use_zrl_cb_img_x'], $s);
}
$s = preg_replace_callback("/([^\]\=\{\/]|^)(https?\:\/\/)([a-zA-Z0-9\pL\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,\@\(\)]+)/ismu", ['\\Code\\Lib\\Markdown', 'use_zrl_cb_link'], $s);
} else {
$s = preg_replace("/([^\]\=\{\/]|^)(https?\:\/\/)([a-zA-Z0-9\pL\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,\@\(\)]+)/ismu", '$1[url=$2$3]$2$3[/url]', $s);
}
// remove duplicate adjacent code tags
$s = preg_replace("/(\[code\])+(.*?)(\[\/code\])+/ism", "[code]$2[/code]", $s);
/**
* @hooks markdown_to_bb
* * \e string - The already converted message as bbcode
*/
Hook::call('markdown_to_bb', $s);
return $s;
}
public static function use_zrl_cb_link($match)
{
$res = '';
$is_zid = is_matrix_url(trim($match[0]));
if ($is_zid) {
$res = $match[1] . '[zrl=' . $match[2] . $match[3] . ']' . $match[2] . $match[3] . '[/zrl]';
} else {
$res = $match[1] . '[url=' . $match[2] . $match[3] . ']' . $match[2] . $match[3] . '[/url]';
}
return $res;
}
public static function use_zrl_cb_img($match)
{
$res = '';
$is_zid = is_matrix_url(trim($match[1]));
if ($is_zid) {
$res = '[zmg]' . $match[1] . '[/zmg]';
} else {
$res = $match[0];
}
return $res;
}
public static function use_zrl_cb_img_x($match)
{
$res = '';
$is_zid = is_matrix_url(trim($match[3]));
if ($is_zid) {
$res = '[zmg=' . $match[1] . 'x' . $match[2] . ']' . $match[3] . '[/zmg]';
} else {
$res = $match[0];
}
return $res;
}
/**
* @brief
*
* @param array $match
* @return string
*/
public static function from_bbcode_share($match)
{
$matches = [];
$attributes = $match[1];
$author = "";
preg_match("/author='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "") {
$author = urldecode($matches[1]);
}
$link = "";
preg_match("/link='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "") {
$link = $matches[1];
}
$avatar = "";
preg_match("/avatar='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "") {
$avatar = $matches[1];
}
$profile = "";
preg_match("/profile='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "") {
$profile = $matches[1];
}
$posted = "";
preg_match("/posted='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "") {
$posted = $matches[1];
}
// message_id is never used, do we still need it?
$message_id = "";
preg_match("/message_id='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "") {
$message_id = $matches[1];
}
if (!$message_id) {
preg_match("/guid='(.*?)'/ism", $attributes, $matches);
if ($matches[1] != "") {
$message_id = $matches[1];
}
}
$reldate = datetime_convert('UTC', date_default_timezone_get(), $posted, 'r');
$headline = '';
if ($avatar != "") {
$headline .= '[url=' . zid($profile) . '][img]' . $avatar . '[/img][/url]';
}
// Bob Smith wrote the following post 2 hours ago
$fmt = sprintf(
t('%1$s wrote the following %2$s %3$s'),
'[url=' . zid($profile) . ']' . $author . '[/url]',
'[url=' . zid($link) . ']' . t('post') . '[/url]',
$reldate
);
$headline .= $fmt . "\n\n";
$text = $headline . trim($match[2]);
return $text;
}
/**
* @brief Convert bbcode to Markdown.
*
* @param string $Text The message as bbcode
* @param array $options default empty
* @return string The message converted to Markdown
*/
public static function from_bbcode($Text, $options = [])
{
/*
* Transform #tags, strip off the [url] and replace spaces with underscore
*/
$Text = preg_replace_callback(
'/#\[([zu])rl\=(.*?)\](.*?)\[\/[(zu)]rl\]/i',
create_function('$match', 'return \'#\'. str_replace(\' \', \'_\', $match[3]);'),
$Text
);
$Text = preg_replace('/#\^\[([zu])rl\=(.*?)\](.*?)\[\/([zu])rl\]/i', '[$1rl=$2]$3[/$4rl]', $Text);
// Converting images with size parameters to simple images. Markdown doesn't know it.
$Text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $Text);
$Text = preg_replace_callback("/\[share(.*?)\](.*?)\[\/share\]/ism", ['\\Code\\Lib\\Markdown', 'from_bbcode_share'], $Text);
$x = ['bbcode' => $Text, 'options' => $options];
/**
* @hooks bb_to_markdown_bb
* * \e string \b bbcode - The message as bbcode and what will get returned
* * \e array \b options
*/
Hook::call('bb_to_markdown_bb', $x);
$Text = $x['bbcode'];
// Convert it to HTML - don't try oembed
$Text = bbcode($Text, ['tryoembed' => false]);
// Now convert HTML to Markdown
$Text = self::from_html($Text);
//html2markdown adds backslashes infront of hashes after a new line. remove them
$Text = str_replace("\n\#", "\n#", $Text);
// If the text going into bbcode() has a plain URL in it, i.e.
// with no [url] tags around it, it will come out of parseString()
// looking like: <http://url.com>, which gets removed by strip_tags().
// So take off the angle brackets of any such URL
$Text = preg_replace("/<http(.*?)>/is", "http$1", $Text);
// Remove empty zrl links
$Text = preg_replace("/\[zrl\=\].*?\[\/zrl\]/is", "", $Text);
$Text = trim($Text);
/**
* @hooks bb_to_markdown
* * \e string - The already converted message as bbcode and what will get returned
*/
Hook::call('bb_to_markdown', $Text);
return $Text;
}
/**
* @brief Convert a HTML text into Markdown.
*
* This function uses the library league/html-to-markdown for this task.
*
* If the HTML text can not get parsed it will return an empty string.
*
* @param string $html The HTML code to convert
* @return string Markdown representation of the given HTML text, empty on error
*/
public static function from_html($html, $options = [])
{
$markdown = '';
if (!$options) {
$options = [
'header_style' => 'setext', // Set to 'atx' to output H1 and H2 headers as # Header1 and ## Header2
'suppress_errors' => true, // Set to false to show warnings when loading malformed HTML
'strip_tags' => false, // Set to true to strip tags that don't have markdown equivalents. N.B. Strips tags, not their content. Useful to clean MS Word HTML output.
'bold_style' => '**', // DEPRECATED: Set to '__' if you prefer the underlined style
'italic_style' => '*', // DEPRECATED: Set to '_' if you prefer the underlined style
'remove_nodes' => '', // space-separated list of dom nodes that should be removed. example: 'meta style script'
'hard_break' => false, // Set to true to turn <br> into `\n` instead of ` \n`
'list_item_style' => '-', // Set the default character for each <li> in a <ul>. Can be '-', '*', or '+'
];
}
$environment = Environment::createDefaultEnvironment($options);
$environment->addConverter(new TableConverter());
$converter = new HtmlConverter($environment);
try {
$markdown = $converter->convert($html);
} catch (InvalidArgumentException $e) {
logger("Invalid HTML. HTMLToMarkdown library threw an exception.");
}
return $markdown;
}
}
// Tables are not an official part of the markdown specification.
// This interface was suggested as a workaround.
// author: Mark Hamstra
// https://github.com/Mark-H/Docs
class TableConverter implements ConverterInterface
{
/**
* @param ElementInterface $element
*
* @return string
*/
public function convert(ElementInterface $element)
{
switch ($element->getTagName()) {
case 'tr':
$line = [];
$i = 1;
foreach ($element->getChildren() as $td) {
$i++;
$v = $td->getValue();
$v = trim($v);
if ($i % 2 === 0 || $v !== '') {
$line[] = $v;
}
}
return '| ' . implode(' | ', $line) . " |\n";
case 'td':
case 'th':
return trim($element->getValue());
case 'tbody':
return trim($element->getValue());
case 'thead':
$headerLine = reset($element->getChildren())->getValue();
$headers = explode(' | ', trim(trim($headerLine, "\n"), '|'));
$hr = [];
foreach ($headers as $td) {
$length = strlen(trim($td)) + 2;
$hr[] = str_repeat('-', $length > 3 ? $length : 3);
}
$hr = '|' . implode('|', $hr) . '|';
return $headerLine . $hr . "\n";
case 'table':
$inner = $element->getValue();
if (strpos($inner, '-----') === false) {
$inner = explode("\n", $inner);
$single = explode(' | ', trim($inner[0], '|'));
$hr = [];
foreach ($single as $td) {
$length = strlen(trim($td)) + 2;
$hr[] = str_repeat('-', $length > 3 ? $length : 3);
}
$hr = '|' . implode('|', $hr) . '|';
array_splice($inner, 1, 0, $hr);
$inner = implode("\n", $inner);
}
return trim($inner) . "\n\n";
}
return $element->getValue();
}
/**
* @return string[]
*/
public function getSupportedTags()
{
return array('table', 'tr', 'thead', 'td', 'tbody');
}
}

147
Code/Lib/MarkdownSoap.php Normal file
View file

@ -0,0 +1,147 @@
<?php
namespace Code\Lib;
/**
* @brief MarkdownSoap class.
*
* Purify Markdown for storage
* @code{.php}
* $x = new MarkdownSoap($string_to_be_cleansed);
* $text = $x->clean();
* @endcode
* What this does:
* 1. extracts code blocks and privately escapes them from processing
* 2. Run html purifier on the content
* 3. put back the code blocks
* 4. run htmlspecialchars on the entire content for safe storage
*
* At render time:
* @code{.php}
* $markdown = \Code\Lib\MarkdownSoap::unescape($text);
* $html = \Michelf\MarkdownExtra::DefaultTransform($markdown);
* @endcode
*/
class MarkdownSoap
{
/**
* @var string
*/
private $str;
/**
* @var string
*/
private $token;
public function __construct($s)
{
$this->str = $s;
$this->token = random_string(20);
}
public function clean()
{
$x = $this->extract_code($this->str);
$x = $this->purify($x);
$x = $this->putback_code($x);
$x = $this->escape($x);
return $x;
}
/**
* @brief Extracts code blocks and privately escapes them from processing.
*
* @param string $s
* @return string
* @see encode_code()
* @see putback_code()
*
*/
public function extract_code($s)
{
$text = preg_replace_callback(
'{
(?:\n\n|\A\n?)
( # $1 = the code block -- one or more lines, starting with a space/tab
(?>
[ ]{' . '4' . '} # Lines must start with a tab or a tab-width of spaces
.*\n+
)+
)
((?=^[ ]{0,' . '4' . '}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
}xm',
[$this, 'encode_code'],
$s
);
return $text;
}
public function encode_code($matches)
{
return $this->token . ';' . base64_encode($matches[0]) . ';';
}
public function decode_code($matches)
{
return base64_decode($matches[1]);
}
/**
* @brief Put back the code blocks.
*
* @param string $s
* @return string
* @see extract_code()
* @see decode_code()
*
*/
public function putback_code($s)
{
$text = preg_replace_callback('{' . $this->token . '\;(.*?)\;}xm', [$this, 'decode_code'], $s);
return $text;
}
public function purify($s)
{
$s = $this->protect_autolinks($s);
$s = purify_html($s);
$s = $this->unprotect_autolinks($s);
return $s;
}
public function protect_autolinks($s)
{
$s = preg_replace('/\<(https?\:\/\/)(.*?)\>/', '[$1$2]($1$2)', $s);
return $s;
}
public function unprotect_autolinks($s)
{
return $s;
}
public function escape($s)
{
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8', false);
}
/**
* @brief Converts special HTML entities back to characters.
*
* @param string $s
* @return string
*/
public static function unescape($s)
{
return htmlspecialchars_decode($s, ENT_QUOTES);
}
}

109
Code/Lib/MastAPI.php Normal file
View file

@ -0,0 +1,109 @@
<?php
namespace Code\Lib;
use App;
use Code\Lib\PConfig;
use Code\Lib\Channel;
class MastAPI
{
public static function format_channel($channel)
{
$p = q(
"select * from profile where uid = %d and is_default = 1",
intval($channel['channel_id'])
);
$a = q(
"select * from account where account_id = %d",
intval($channel['channel_account_id'])
);
$followers = q(
"select count(xchan_hash) as total from xchan left join abconfig on abconfig.xchan = xchan_hash left join abook on abook_xchan = xchan_hash where abook_channel = %d and abconfig.chan = %d and abconfig.cat = 'system' and abconfig.k = 'their_perms' and abconfig.v like '%%send_stream%%' and xchan_hash != '%s' and xchan_orphan = 0 and xchan_deleted = 0 and abook_hidden = 0 and abook_pending = 0 and abook_self = 0 ",
intval($channel['channel_id']),
intval($channel['channel_id']),
dbesc($channel['channel_hash'])
);
$following = q(
"select count(xchan_hash) as total from xchan left join abconfig on abconfig.xchan = xchan_hash left join abook on abook_xchan = xchan_hash where abook_channel = %d and abconfig.chan = %d and abconfig.cat = 'system' and abconfig.k = 'my_perms' and abconfig.v like '%%send_stream%%' and xchan_hash != '%s' and xchan_orphan = 0 and xchan_deleted = 0 and abook_hidden = 0 and abook_pending = 0 and abook_self = 0",
intval($channel['channel_id']),
intval($channel['channel_id']),
dbesc($channel['channel_hash'])
);
$cover_photo = Channel::get_cover_photo($channel['channel_id'], 'array');
$item_normal = item_normal();
// count posts/comments
$statuses = q(
"SELECT COUNT(id) as total FROM item
WHERE uid = %d
AND author_xchan = '%s' $item_normal ",
intval($channel['channel_id']),
dbesc($channel['channel_hash'])
);
$ret = [];
$ret['id'] = (string)$channel['channel_id'];
$ret['username'] = $channel['channel_address'];
$ret['acct'] = $channel['channel_address'];
$ret['display_name'] = $channel['channel_name'];
$ret['locked'] = ((intval(PConfig::Get($channel['channel_id'], 'system', 'autoperms'))) ? false : true);
$ret['discoverable'] = ((1 - intval($channel['xchan_hidden'])) ? true : false);
$ret['created_at'] = datetime_convert('UTC', 'UTC', $a[0]['account_created'], ATOM_TIME);
$ret['note'] = bbcode($p[0]['about'], ['export' => true]);
$ret['url'] = Channel::url($channel);
$ret['avatar'] = $channel['xchan_photo_l'];
$ret['avatar_static'] = $channel['xchan_photo_l'];
if ($cover_photo) {
$ret['header'] = $cover_photo['url'];
$ret['header_static'] = $cover_photo['url'];
}
$ret['followers_count'] = intval($followers[0]['total']);
$ret['following_count'] = intval($following[0]['total']);
$ret['statuses_count'] = intval($statuses[0]['total']);
$ret['last_status_at'] = datetime_convert('UTC', 'UTC', $channel['lastpost'], ATOM_TIME);
return $ret;
}
public static function format_site()
{
$register = intval(get_config('system', 'register_policy'));
$u = q("select count(channel_id) as total from channel where channel_removed = 0");
$i = q("select count(id) as total from item where item_origin = 1");
$s = q("select count(site_url) as total from site");
$admins = q("select * from channel left join account on account_id = channel_account_id where ( account_roles & 4096 ) > 0 and account_default_channel = channel_id");
$adminsx = Channel::from_id($admins[0]['channel_id']);
$ret = [];
$ret['uri'] = z_root();
$ret['title'] = System::get_site_name();
$ret['description'] = bbcode(get_config('system', 'siteinfo', ''), ['export' => true]);
$ret['email'] = get_config('system', 'admin_email');
$ret['version'] = System::get_project_version();
$ret['registrations'] = (($register) ? true : false);
$ret['approval_required'] = (($register === REGISTER_APPROVE) ? true : false);
$ret['invites_enabled'] = false;
$ret['urls'] = [];
$ret['stats'] = [
'user_count' => intval($u[0]['total']),
'status_count' => intval($i[0]['total']),
'domain_count' => intval($s[0]['total']),
];
$ret['contact_account'] = self::format_channel($adminsx);
return $ret;
}
}

367
Code/Lib/Menu.php Normal file
View file

@ -0,0 +1,367 @@
<?php
namespace Code\Lib;
use Code\Lib\Libsync;
use Code\Lib\Channel;
use Code\Render\Theme;
require_once('include/security.php');
require_once('include/bbcode.php');
class Menu
{
public static function fetch($name, $uid, $observer_xchan)
{
$sql_options = permissions_sql($uid, $observer_xchan);
$r = q(
"select * from menu where menu_channel_id = %d and menu_name = '%s' limit 1",
intval($uid),
dbesc($name)
);
if ($r) {
$x = q(
"select * from menu_item where mitem_menu_id = %d and mitem_channel_id = %d
$sql_options
order by mitem_order asc, mitem_desc asc",
intval($r[0]['menu_id']),
intval($uid)
);
return array('menu' => $r[0], 'items' => $x );
}
return null;
}
public static function element($channel, $menu)
{
$arr = [];
$arr['type'] = 'menu';
$arr['pagetitle'] = $menu['menu']['menu_name'];
$arr['desc'] = $menu['menu']['menu_desc'];
$arr['created'] = $menu['menu']['menu_created'];
$arr['edited'] = $menu['menu']['menu_edited'];
$arr['baseurl'] = z_root();
if ($menu['menu']['menu_flags']) {
$arr['flags'] = [];
if ($menu['menu']['menu_flags'] & MENU_BOOKMARK) {
$arr['flags'][] = 'bookmark';
}
if ($menu['menu']['menu_flags'] & MENU_SYSTEM) {
$arr['flags'][] = 'system';
}
}
if ($menu['items']) {
$arr['items'] = [];
foreach ($menu['items'] as $it) {
$entry = [];
$entry['link'] = str_replace(z_root() . '/channel/' . $channel['channel_address'], '[channelurl]', $it['mitem_link']);
$entry['link'] = str_replace(z_root() . '/page/' . $channel['channel_address'], '[pageurl]', $it['mitem_link']);
$entry['link'] = str_replace(z_root() . '/cloud/' . $channel['channel_address'], '[cloudurl]', $it['mitem_link']);
$entry['link'] = str_replace(z_root(), '[baseurl]', $it['mitem_link']);
$entry['desc'] = $it['mitem_desc'];
$entry['order'] = $it['mitem_order'];
if ($it['mitem_flags']) {
$entry['flags'] = [];
if ($it['mitem_flags'] & MENU_ITEM_ZID) {
$entry['flags'][] = 'zid';
}
if ($it['mitem_flags'] & MENU_ITEM_NEWWIN) {
$entry['flags'][] = 'new-window';
}
if ($it['mitem_flags'] & MENU_ITEM_CHATROOM) {
$entry['flags'][] = 'chatroom';
}
}
$arr['items'][] = $entry;
}
}
return $arr;
}
public static function render($menu, $class = '', $edit = false, $var = [])
{
if (! $menu) {
return '';
}
$channel_id = ((is_array(App::$profile)) ? App::$profile['profile_uid'] : 0);
if ((! $channel_id) && (local_channel())) {
$channel_id = local_channel();
}
$chan = Channel::from_id($channel_id);
if (! $chan) {
return '';
}
$menu_list = self::list($channel_id);
$menu_names = [];
foreach ($menu_list as $menus) {
if ($menus['menu_name'] != $menu['menu']['menu_name']) {
$menu_names[] = $menus['menu_name'];
}
}
for ($x = 0; $x < count($menu['items']); $x++) {
if (in_array($menu['items'][$x]['mitem_link'], $menu_names)) {
$m = self::fetch($menu['items'][$x]['mitem_link'], $channel_id, get_observer_hash());
$submenu = self::render($m, 'dropdown-menu', $edit = false, array('wrap' => 'none'));
$menu['items'][$x]['submenu'] = $submenu;
}
if ($menu['items'][$x]['mitem_flags'] & MENU_ITEM_ZID) {
$menu['items'][$x]['mitem_link'] = zid($menu['items'][$x]['mitem_link']);
}
if ($menu['items'][$x]['mitem_flags'] & MENU_ITEM_NEWWIN) {
$menu['items'][$x]['newwin'] = '1';
}
$menu['items'][$x]['mitem_desc'] = zidify_links(smilies(bbcode($menu['items'][$x]['mitem_desc'])));
}
$wrap = (($var['wrap'] === 'none') ? false : true);
$ret = replace_macros(Theme::get_template('usermenu.tpl'), array(
'$menu' => $menu['menu'],
'$class' => $class,
'$nick' => $chan['channel_address'],
'$edit' => (($edit) ? t("Edit") : ''),
'$id' => $menu['menu']['menu_id'],
'$items' => $menu['items'],
'$wrap' => $wrap
));
return $ret;
}
public static function fetch_id($menu_id, $channel_id)
{
$r = q(
"select * from menu where menu_id = %d and menu_channel_id = %d limit 1",
intval($menu_id),
intval($channel_id)
);
return (($r) ? $r[0] : false);
}
public static function create($arr)
{
$menu_name = trim(escape_tags($arr['menu_name']));
$menu_desc = trim(escape_tags($arr['menu_desc']));
$menu_flags = intval($arr['menu_flags']);
//allow menu_desc (title) to be empty
//if(! $menu_desc)
// $menu_desc = $menu_name;
if (! $menu_name) {
return false;
}
if (! $menu_flags) {
$menu_flags = 0;
}
$menu_channel_id = intval($arr['menu_channel_id']);
$r = q(
"select * from menu where menu_name = '%s' and menu_channel_id = %d limit 1",
dbesc($menu_name),
intval($menu_channel_id)
);
if ($r) {
return false;
}
$t = datetime_convert();
$r = q(
"insert into menu ( menu_name, menu_desc, menu_flags, menu_channel_id, menu_created, menu_edited )
values( '%s', '%s', %d, %d, '%s', '%s' )",
dbesc($menu_name),
dbesc($menu_desc),
intval($menu_flags),
intval($menu_channel_id),
dbesc(datetime_convert('UTC', 'UTC', (($arr['menu_created']) ? $arr['menu_created'] : $t))),
dbesc(datetime_convert('UTC', 'UTC', (($arr['menu_edited']) ? $arr['menu_edited'] : $t)))
);
if (! $r) {
return false;
}
$r = q(
"select menu_id from menu where menu_name = '%s' and menu_channel_id = %d limit 1",
dbesc($menu_name),
intval($menu_channel_id)
);
if ($r) {
return $r[0]['menu_id'];
}
return false;
}
/**
* If $flags is present, check that all the bits in $flags are set
* so that MENU_SYSTEM|MENU_BOOKMARK will return entries with both
* bits set. We will use this to find system generated bookmarks.
*/
public static function list($channel_id, $name = '', $flags = 0)
{
$sel_options = '';
$sel_options .= (($name) ? " and menu_name = '" . protect_sprintf(dbesc($name)) . "' " : '');
$sel_options .= (($flags) ? " and menu_flags = " . intval($flags) . " " : '');
$r = q(
"select * from menu where menu_channel_id = %d $sel_options order by menu_desc",
intval($channel_id)
);
return $r;
}
public static function list_count($channel_id, $name = '', $flags = 0)
{
$sel_options = '';
$sel_options .= (($name) ? " and menu_name = '" . protect_sprintf(dbesc($name)) . "' " : '');
$sel_options .= (($flags) ? " and menu_flags = " . intval($flags) . " " : '');
$r = q(
"select count(*) as total from menu where menu_channel_id = %d $sel_options",
intval($channel_id)
);
return $r[0]['total'];
}
public static function edit($arr)
{
$menu_id = intval($arr['menu_id']);
$menu_name = trim(escape_tags($arr['menu_name']));
$menu_desc = trim(escape_tags($arr['menu_desc']));
$menu_flags = intval($arr['menu_flags']);
//allow menu_desc (title) to be empty
//if(! $menu_desc)
// $menu_desc = $menu_name;
if (! $menu_name) {
return false;
}
if (! $menu_flags) {
$menu_flags = 0;
}
$menu_channel_id = intval($arr['menu_channel_id']);
$r = q(
"select menu_id from menu where menu_name = '%s' and menu_channel_id = %d limit 1",
dbesc($menu_name),
intval($menu_channel_id)
);
if (($r) && ($r[0]['menu_id'] != $menu_id)) {
logger('menu_edit: duplicate menu name for channel ' . $menu_channel_id);
return false;
}
$r = q(
"select * from menu where menu_id = %d and menu_channel_id = %d limit 1",
intval($menu_id),
intval($menu_channel_id)
);
if (! $r) {
logger('menu_edit: not found: ' . print_r($arr, true));
return false;
}
return q(
"update menu set menu_name = '%s', menu_desc = '%s', menu_flags = %d, menu_edited = '%s'
where menu_id = %d and menu_channel_id = %d",
dbesc($menu_name),
dbesc($menu_desc),
intval($menu_flags),
dbesc(datetime_convert()),
intval($menu_id),
intval($menu_channel_id)
);
}
public static function delete($menu_name, $uid)
{
$r = q(
"select menu_id from menu where menu_name = '%s' and menu_channel_id = %d limit 1",
dbesc($menu_name),
intval($uid)
);
if ($r) {
return self::delete_id($r[0]['menu_id'], $uid);
}
return false;
}
public static function delete_id($menu_id, $uid)
{
$r = q(
"select menu_id from menu where menu_id = %d and menu_channel_id = %d limit 1",
intval($menu_id),
intval($uid)
);
if ($r) {
$x = q(
"delete from menu_item where mitem_menu_id = %d and mitem_channel_id = %d",
intval($menu_id),
intval($uid)
);
return q(
"delete from menu where menu_id = %d and menu_channel_id = %d",
intval($menu_id),
intval($uid)
);
}
return false;
}
public static function sync_packet($uid, $observer_hash, $menu_id, $delete = false)
{
$r = self::fetch_id($menu_id, $uid);
$c = Channel::from_id($uid);
if ($r) {
$m = self::fetch($r['menu_name'], $uid, $observer_hash);
if ($m) {
if ($delete) {
$m['menu_delete'] = 1;
}
Libsync::build_sync_packet($uid, array('menu' => array(menu_element($c, $m))));
}
}
}
}

122
Code/Lib/MenuItem.php Normal file
View file

@ -0,0 +1,122 @@
<?php
namespace Code\Lib;
use Code\Lib\Libsync;
use Code\Lib\Channel;
use Code\Access\AccessControl;
require_once('include/security.php');
require_once('include/bbcode.php');
class MenuItem
{
public static function add($menu_id, $uid, $arr)
{
$mitem_link = escape_tags($arr['mitem_link']);
$mitem_desc = escape_tags($arr['mitem_desc']);
$mitem_order = intval($arr['mitem_order']);
$mitem_flags = intval($arr['mitem_flags']);
if (local_channel() == $uid) {
$channel = App::get_channel();
}
$acl = new AccessControl($channel);
$acl->set_from_array($arr);
$p = $acl->get();
$r = q(
"insert into menu_item ( mitem_link, mitem_desc, mitem_flags, allow_cid, allow_gid, deny_cid, deny_gid, mitem_channel_id, mitem_menu_id, mitem_order ) values ( '%s', '%s', %d, '%s', '%s', '%s', '%s', %d, %d, %d ) ",
dbesc($mitem_link),
dbesc($mitem_desc),
intval($mitem_flags),
dbesc($p['allow_cid']),
dbesc($p['allow_gid']),
dbesc($p['deny_cid']),
dbesc($p['deny_gid']),
intval($uid),
intval($menu_id),
intval($mitem_order)
);
$x = q(
"update menu set menu_edited = '%s' where menu_id = %d and menu_channel_id = %d",
dbesc(datetime_convert()),
intval($menu_id),
intval($uid)
);
return $r;
}
public static function edit($menu_id, $uid, $arr)
{
$mitem_id = intval($arr['mitem_id']);
$mitem_link = escape_tags($arr['mitem_link']);
$mitem_desc = escape_tags($arr['mitem_desc']);
$mitem_order = intval($arr['mitem_order']);
$mitem_flags = intval($arr['mitem_flags']);
if (local_channel() == $uid) {
$channel = App::get_channel();
}
$acl = new AccessControl($channel);
$acl->set_from_array($arr);
$p = $acl->get();
$r = q(
"update menu_item set mitem_link = '%s', mitem_desc = '%s', mitem_flags = %d, allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s', mitem_order = %d where mitem_channel_id = %d and mitem_menu_id = %d and mitem_id = %d",
dbesc($mitem_link),
dbesc($mitem_desc),
intval($mitem_flags),
dbesc($p['allow_cid']),
dbesc($p['allow_gid']),
dbesc($p['deny_cid']),
dbesc($p['deny_gid']),
intval($mitem_order),
intval($uid),
intval($menu_id),
intval($mitem_id)
);
$x = q(
"update menu set menu_edited = '%s' where menu_id = %d and menu_channel_id = %d",
dbesc(datetime_convert()),
intval($menu_id),
intval($uid)
);
return $r;
}
public static function delete($menu_id, $uid, $item_id)
{
$r = q(
"delete from menu_item where mitem_menu_id = %d and mitem_channel_id = %d and mitem_id = %d",
intval($menu_id),
intval($uid),
intval($item_id)
);
$x = q(
"update menu set menu_edited = '%s' where menu_id = %d and menu_channel_id = %d",
dbesc(datetime_convert()),
intval($menu_id),
intval($uid)
);
return $r;
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace Code\Lib;
class MessageFilter
{
public static function evaluate($item, $incl, $excl)
{
require_once('include/html2plain.php');
$text = prepare_text($item['body'],((isset($item['mimetype'])) ? $item['mimetype'] : 'text/x-multicode'));
$text = html2plain(($item['title']) ? $item['title'] . ' ' . $text : $text);
$lang = null;
if ((strpos($incl, 'lang=') !== false) || (strpos($excl, 'lang=') !== false) || (strpos($incl, 'lang!=') !== false) || (strpos($excl, 'lang!=') !== false)) {
$lang = detect_language($text);
}
$tags = ((isset($item['term']) && is_array($item['term']) && count($item['term'])) ? $item['term'] : false);
// exclude always has priority
$exclude = (($excl) ? explode("\n", $excl) : null);
if ($exclude) {
foreach ($exclude as $word) {
$word = trim($word);
if (! $word) {
continue;
}
if (substr($word, 0, 1) === '#' && $tags) {
foreach ($tags as $t) {
if ((($t['ttype'] == TERM_HASHTAG) || ($t['ttype'] == TERM_COMMUNITYTAG)) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) {
return false;
}
}
} elseif (substr($word, 0, 1) === '$' && $tags) {
foreach ($tags as $t) {
if (($t['ttype'] == TERM_CATEGORY) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) {
return false;
}
}
} elseif ((strpos($word, '/') === 0) && preg_match($word, $text)) {
return false;
} elseif ((strpos($word, 'lang=') === 0) && ($lang) && (strcasecmp($lang, trim(substr($word, 5))) == 0)) {
return false;
} elseif ((strpos($word, 'lang!=') === 0) && ($lang) && (strcasecmp($lang, trim(substr($word, 6))) != 0)) {
return false;
} elseif (stristr($text, $word) !== false) {
return false;
}
}
}
$include = (($incl) ? explode("\n", $incl) : null);
if ($include) {
foreach ($include as $word) {
$word = trim($word);
if (! $word) {
continue;
}
if (substr($word, 0, 1) === '#' && $tags) {
foreach ($tags as $t) {
if ((($t['ttype'] == TERM_HASHTAG) || ($t['ttype'] == TERM_COMMUNITYTAG)) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) {
return true;
}
}
} elseif (substr($word, 0, 1) === '$' && $tags) {
foreach ($tags as $t) {
if (($t['ttype'] == TERM_CATEGORY) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) {
return true;
}
}
} elseif ((strpos($word, '/') === 0) && preg_match($word, $text)) {
return true;
} elseif ((strpos($word, 'lang=') === 0) && ($lang) && (strcasecmp($lang, trim(substr($word, 5))) == 0)) {
return true;
} elseif ((strpos($word, 'lang!=') === 0) && ($lang) && (strcasecmp($lang, trim(substr($word, 6))) != 0)) {
return true;
} elseif (stristr($text, $word) !== false) {
return true;
}
}
} else {
return true;
}
return false;
}
}

556
Code/Lib/Navbar.php Normal file
View file

@ -0,0 +1,556 @@
<?php
namespace Code\Lib;
use App;
use Code\Lib\Apps;
use Code\Lib\Chatroom;
use Code\Lib\Channel;
use Code\Lib\System;
use Code\Lib\Features;
use Code\Lib\Menu;
use Code\Lib\Head;
use Code\Render\Theme;
use Code\Extend\Hook;
require_once('include/security.php');
class Navbar {
public static function render($template = 'default')
{
/**
*
* Build page header and site navigation bars
*
*/
if (! isset(App::$page['nav'])) {
App::$page['nav'] = EMPTY_STR;
}
if (! isset(App::$page['htmlhead'])) {
App::$page['htmlhead'] = EMPTY_STR;
}
$site_channel = Channel::get_system();
App::$page['htmlhead'] .= '<script>$(document).ready(function() { $("#nav-search-text").search_autocomplete(\'' . z_root() . '/acloader' . '\');});</script>';
$is_owner = (((local_channel()) && ((App::$profile_uid == local_channel()) || (App::$profile_uid == 0))) ? true : false);
if (local_channel()) {
$channel = App::get_channel();
$observer = App::get_observer();
$prof = q(
"select id from profile where uid = %d and is_default = 1",
intval($channel['channel_id'])
);
if (! (isset($_SESSION['delegate']) && $_SESSION['delegate'])) {
$chans = q(
"select channel_name, channel_id from channel left join pconfig on channel_id = pconfig.uid where channel_account_id = %d and channel_removed = 0 and pconfig.cat = 'system' and pconfig.k = 'include_in_menu' and pconfig.v = '1' order by channel_name ",
intval(get_account_id())
);
if (is_site_admin() && intval(get_pconfig($site_channel['channel_id'], 'system', 'include_in_menu'))) {
$chans = array_merge([$site_channel], $chans);
}
}
$sitelocation = (($is_owner) ? '' : App::$profile['reddress']);
} elseif (remote_channel()) {
$observer = App::get_observer();
$sitelocation = ((App::$profile['reddress']) ? App::$profile['reddress'] : '@' . App::get_hostname());
}
require_once('include/conversation.php');
$channel_apps[] = ((isset(App::$profile)) ? self::channel_apps($is_owner, App::$profile['channel_address']) : []);
$site_icon = System::get_site_icon();
$banner = EMPTY_STR;
// $banner = System::get_site_name();
// if (! isset(App::$page['header'])) {
// App::$page['header'] = EMPTY_STR;
// }
App::$page['header'] .= replace_macros(Theme::get_template('hdr.tpl'), array(
//we could additionally use this to display important system notifications e.g. for updates
));
// nav links: array of array('href', 'text', 'extra css classes', 'title')
$nav = [];
if (can_view_public_stream()) {
$nav['pubs'] = true;
}
/**
* Display login or logout
*/
$nav['usermenu'] = [];
$userinfo = null;
$nav['loginmenu'] = [];
if ($observer) {
$userinfo = [
'icon' => $observer['xchan_photo_m'] . '?rev=' . strtotime($observer['xchan_photo_date']),
'name' => $observer['xchan_addr'],
];
} elseif (! $_SESSION['authenticated']) {
$nav['remote_login'] = Channel::remote_login();
$nav['loginmenu'][] = array('rmagic',t('Remote authentication'),'',t('Click to authenticate to your home hub'),'rmagic_nav_btn');
}
if (local_channel()) {
if (! (isset($_SESSION['delegate']) && $_SESSION['delegate'])) {
$nav['manage'] = array('manage', t('Channels'), "", t('Manage your channels'),'manage_nav_btn');
}
$nav['group'] = array('lists', t('Lists'),"", t('Manage your access lists'),'group_nav_btn');
$nav['settings'] = array('settings', t('Settings'),"", t('Account/Channel Settings'),'settings_nav_btn');
$nav['safe'] = array('safe', t('Safe Mode'), ((get_safemode()) ? t('(is on)') : t('(is off)')) , t('Content filtering'),'safe_nav_btn');
if ($chans && count($chans) > 0) {
$nav['channels'] = $chans;
}
$nav['logout'] = ['logout',t('Logout'), "", t('End this session'),'logout_nav_btn'];
// user menu
$nav['usermenu'][] = ['profile/' . $channel['channel_address'], t('View Profile'), ((App::$nav_sel['raw_name'] == 'Profile') ? 'active' : ''), t('Your profile page'),'profile_nav_btn'];
if (Features::enabled(local_channel(), 'multi_profiles')) {
$nav['usermenu'][] = ['profiles', t('Edit Profiles'), ((App::$nav_sel['raw_name'] == 'Profiles') ? 'active' : '') , t('Manage/Edit profiles'),'profiles_nav_btn'];
} else {
$nav['usermenu'][] = ['profiles/' . $prof[0]['id'], t('Edit Profile'), ((App::$nav_sel['raw_name'] == 'Profiles') ? 'active' : ''), t('Edit your profile'),'profiles_nav_btn'];
}
} else {
if (! get_account_id()) {
if (App::$module === 'channel') {
$nav['login'] = login(true, 'navbar-login', false, false);
$nav['loginmenu'][] = ['login',t('Login'),'',t('Sign in'),''];
} else {
$nav['login'] = login(true, 'navbar-login', false, false);
$nav['loginmenu'][] = ['login',t('Login'),'',t('Sign in'),'login_nav_btn'];
App::$page['content'] .= replace_macros(
Theme::get_template('nav_login.tpl'),
[
'$nav' => $nav,
'userinfo' => $userinfo
]
);
}
} else {
$nav['alogout'] = ['logout',t('Logout'), "", t('End this session'),'logout_nav_btn'];
}
}
$my_url = Channel::get_my_url();
if (! $my_url) {
$observer = App::get_observer();
$my_url = (($observer) ? $observer['xchan_url'] : '');
}
$homelink_arr = parse_url($my_url);
$homelink = $homelink_arr['scheme'] . '://' . $homelink_arr['host'];
if (! $is_owner) {
$nav['rusermenu'] = array(
$homelink,
t('Take me home'),
'logout',
((local_channel()) ? t('Logout') : t('Log me out of this site'))
);
}
if (((get_config('system', 'register_policy') == REGISTER_OPEN) || (get_config('system', 'register_policy') == REGISTER_APPROVE)) && (! $_SESSION['authenticated'])) {
$nav['register'] = ['register',t('Register'), "", t('Create an account'),'register_nav_btn'];
}
if (! get_config('system', 'hide_help', true)) {
$help_url = z_root() . '/help?f=&cmd=' . App::$cmd;
$context_help = '';
$enable_context_help = ((intval(get_config('system', 'enable_context_help')) === 1 || get_config('system', 'enable_context_help') === false) ? true : false);
if ($enable_context_help === true) {
require_once('include/help.php');
$context_help = load_context_help();
//point directly to /help if $context_help is empty - this can be removed once we have context help for all modules
$enable_context_help = (($context_help) ? true : false);
}
$nav['help'] = [$help_url, t('Help'), "", t('Help and documentation'), 'help_nav_btn', $context_help, $enable_context_help];
}
$search_form_action = 'search';
$nav['search'] = ['search', t('Search'), "", t('Search site @name, #tag, ?doc, content'), $search_form_action];
/**
* Admin page
*/
if (is_site_admin()) {
$nav['admin'] = array('admin/', t('Admin'), "", t('Site Setup and Configuration'),'admin_nav_btn');
}
$x = array('nav' => $nav, 'usermenu' => $userinfo );
Hook::call('nav', $x);
// Not sure the best place to put this on the page. So I'm implementing it but leaving it
// turned off until somebody discovers this and figures out a good location for it.
$powered_by = '';
if (App::$profile_uid && App::$nav_sel['raw_name']) {
$active_app = q(
"SELECT app_url FROM app WHERE app_channel = %d AND app_name = '%s' LIMIT 1",
intval(App::$profile_uid),
dbesc(App::$nav_sel['raw_name'])
);
if ($active_app) {
$url = $active_app[0]['app_url'];
}
}
$pinned_list = [];
$syslist = [];
//app bin
if ($is_owner) {
if (get_pconfig(local_channel(), 'system', 'import_system_apps') !== datetime_convert('UTC', 'UTC', 'now', 'Y-m-d')) {
Apps::import_system_apps();
set_pconfig(local_channel(), 'system', 'import_system_apps', datetime_convert('UTC', 'UTC', 'now', 'Y-m-d'));
}
$list = Apps::app_list(local_channel(), false, [ 'nav_pinned_app' ]);
if ($list) {
foreach ($list as $li) {
$pinned_list[] = Apps::app_encode($li);
}
}
Apps::translate_system_apps($pinned_list);
usort($pinned_list, 'Code\\Lib\\Apps::app_name_compare');
$pinned_list = Apps::app_order(local_channel(), $pinned_list, 'nav_pinned_app');
$syslist = [];
$list = Apps::app_list(local_channel(), false, [ 'nav_featured_app' ]);
if ($list) {
foreach ($list as $li) {
$syslist[] = Apps::app_encode($li);
}
}
Apps::translate_system_apps($syslist);
} else {
$syslist = Apps::get_system_apps(true);
}
usort($syslist, 'Code\\Lib\\Apps::app_name_compare');
$syslist = Apps::app_order(local_channel(), $syslist, 'nav_featured_app');
if ($pinned_list) {
foreach ($pinned_list as $app) {
if (App::$nav_sel['name'] == $app['name']) {
$app['active'] = true;
}
if ($is_owner) {
$navbar_apps[] = Apps::app_render($app, 'navbar');
} elseif (! $is_owner && strpos($app['requires'], 'local_channel') === false) {
$navbar_apps[] = Apps::app_render($app, 'navbar');
}
}
}
if ($syslist) {
foreach ($syslist as $app) {
if (App::$nav_sel['name'] == $app['name']) {
$app['active'] = true;
}
if ($is_owner) {
$nav_apps[] = Apps::app_render($app, 'nav');
} elseif (! $is_owner && strpos($app['requires'], 'local_channel') === false) {
$nav_apps[] = Apps::app_render($app, 'nav');
}
}
}
$c = Theme::include('navbar_' . purify_filename($template) . '.css');
$tpl = Theme::get_template('navbar_' . purify_filename($template) . '.tpl');
if ($c && $tpl) {
Head::add_css('navbar_' . $template . '.css');
}
if (! $tpl) {
$tpl = Theme::get_template('navbar_default.tpl');
}
App::$page['nav'] .= replace_macros($tpl, array(
'$baseurl' => z_root(),
'$site_home' => Channel::url($site_channel),
'$project_icon' => $site_icon,
'$project_title' => t('Powered by $Projectname'),
'$fulldocs' => t('Help'),
'$sitelocation' => $sitelocation,
'$nav' => $x['nav'],
'$banner' => $banner,
'$emptynotifications' => t('Loading'),
'$userinfo' => $x['usermenu'],
'$localuser' => local_channel(),
'$is_owner' => $is_owner,
'$sel' => App::$nav_sel,
'$powered_by' => $powered_by,
'$asidetitle' => t('Side Panel'),
'$help' => t('@name, #tag, ?doc, content'),
'$pleasewait' => t('Please wait...'),
'$nav_apps' => ((isset($nav_apps)) ? $nav_apps : []),
'$navbar_apps' => ((isset($navbar_apps)) ? $navbar_apps : []),
'$channel_menu' => get_pconfig(App::$profile_uid, 'system', 'channel_menu', get_config('system', 'channel_menu')),
'$channel_thumb' => ((App::$profile) ? App::$profile['thumb'] : ''),
'$channel_apps' => ((isset($channel_apps)) ? $channel_apps : []),
'$manageapps' => t('Installed Apps'),
'$appstitle' => t('Apps'),
'$addapps' => t('Available Apps'),
'$orderapps' => t('Arrange Apps'),
'$sysapps_toggle' => t('Toggle System Apps'),
'$notificationstitle' => t('Notifications'),
'$url' => ((isset($url) && $url) ? $url : App::$cmd)
));
if (x($_SESSION, 'reload_avatar') && $observer) {
// The avatar has been changed on the server but the browser doesn't know that,
// force the browser to reload the image from the server instead of its cache.
$tpl = Theme::get_template('force_image_reload.tpl');
App::$page['nav'] .= replace_macros($tpl, array(
'$imgUrl' => $observer['xchan_photo_m']
));
unset($_SESSION['reload_avatar']);
}
Hook::call('page_header', App::$page['nav']);
}
/*
* Set a menu item in navbar as selected
*
*/
public static function set_selected($item)
{
App::$nav_sel['raw_name'] = $item;
$item = ['name' => $item];
Apps::translate_system_apps($item);
App::$nav_sel['name'] = $item['name'];
}
public static function channel_apps($is_owner = false, $nickname = null)
{
// Don't provide any channel apps if we're running as the sys channel
if (App::$is_sys) {
return '';
}
$channel = App::get_channel();
if ($channel && is_null($nickname)) {
$nickname = $channel['channel_address'];
}
$uid = ((App::$profile['profile_uid']) ? App::$profile['profile_uid'] : local_channel());
$account_id = ((App::$profile['profile_uid']) ? App::$profile['channel_account_id'] : App::$channel['channel_account_id']);
if (! get_pconfig($uid, 'system', 'channelapps', '1')) {
return '';
}
if ($uid == local_channel()) {
return;
} else {
$cal_link = '/cal/' . $nickname;
}
$sql_options = item_permissions_sql($uid);
$r = q(
"select item.* from item left join iconfig on item.id = iconfig.iid
where item.uid = %d and iconfig.cat = 'system' and iconfig.v = '%s'
and item.item_delayed = 0 and item.item_deleted = 0
and ( iconfig.k = 'WEBPAGE' and item_type = %d )
$sql_options limit 1",
intval($uid),
dbesc('home'),
intval(ITEM_TYPE_WEBPAGE)
);
$has_webpages = (($r) ? true : false);
if (x($_GET, 'tab')) {
$tab = notags(trim($_GET['tab']));
}
$url = z_root() . '/channel/' . $nickname;
$pr = z_root() . '/profile/' . $nickname;
$tabs = [
[
'label' => t('Channel'),
'url' => $url,
'sel' => ((argv(0) == 'channel') ? 'active' : ''),
'title' => t('Status Messages and Posts'),
'id' => 'status-tab',
'icon' => 'home'
],
];
$p = get_all_perms($uid, get_observer_hash());
if ($p['view_profile']) {
$tabs[] = [
'label' => t('About'),
'url' => $pr,
'sel' => ((argv(0) == 'profile') ? 'active' : ''),
'title' => t('Profile Details'),
'id' => 'profile-tab',
'icon' => 'user'
];
}
if ($p['view_storage']) {
$tabs[] = [
'label' => t('Photos'),
'url' => z_root() . '/photos/' . $nickname,
'sel' => ((argv(0) == 'photos') ? 'active' : ''),
'title' => t('Photo Albums'),
'id' => 'photo-tab',
'icon' => 'photo'
];
$tabs[] = [
'label' => t('Files'),
'url' => z_root() . '/cloud/' . $nickname,
'sel' => ((argv(0) == 'cloud' || argv(0) == 'sharedwithme') ? 'active' : ''),
'title' => t('Files and Storage'),
'id' => 'files-tab',
'icon' => 'folder-open'
];
}
if ($p['view_stream'] && $cal_link) {
$tabs[] = [
'label' => t('Calendar'),
'url' => z_root() . $cal_link,
'sel' => ((argv(0) == 'cal') ? 'active' : ''),
'title' => t('Calendar'),
'id' => 'event-tab',
'icon' => 'calendar'
];
}
if ($p['chat'] && Apps::system_app_installed($uid, 'Chatrooms')) {
$has_chats = Chatroom::list_count($uid);
if ($has_chats) {
$tabs[] = [
'label' => t('Chatrooms'),
'url' => z_root() . '/chat/' . $nickname,
'sel' => ((argv(0) == 'chat') ? 'active' : '' ),
'title' => t('Chatrooms'),
'id' => 'chat-tab',
'icon' => 'comments-o'
];
}
}
$has_bookmarks = Menu::list_count(local_channel(), '', MENU_BOOKMARK) + Menu::list_count(local_channel(), '', MENU_SYSTEM | MENU_BOOKMARK);
if ($is_owner && $has_bookmarks) {
$tabs[] = [
'label' => t('Bookmarks'),
'url' => z_root() . '/bookmarks',
'sel' => ((argv(0) == 'bookmarks') ? 'active' : ''),
'title' => t('Saved Bookmarks'),
'id' => 'bookmarks-tab',
'icon' => 'bookmark'
];
}
if ($p['view_pages'] && Apps::system_app_installed($uid, 'Cards')) {
$tabs[] = [
'label' => t('Cards'),
'url' => z_root() . '/cards/' . $nickname ,
'sel' => ((argv(0) == 'cards') ? 'active' : ''),
'title' => t('View Cards'),
'id' => 'cards-tab',
'icon' => 'list'
];
}
if ($p['view_pages'] && Apps::system_app_installed($uid, 'Articles')) {
$tabs[] = [
'label' => t('Articles'),
'url' => z_root() . '/articles/' . $nickname ,
'sel' => ((argv(0) == 'articles') ? 'active' : ''),
'title' => t('View Articles'),
'id' => 'articles-tab',
'icon' => 'file-text-o'
];
}
if ($has_webpages && Apps::system_app_installed($uid, 'Webpages')) {
$tabs[] = [
'label' => t('Webpages'),
'url' => z_root() . '/page/' . $nickname . '/home',
'sel' => ((argv(0) == 'webpages') ? 'active' : ''),
'title' => t('View Webpages'),
'id' => 'webpages-tab',
'icon' => 'newspaper-o'
];
}
if ($p['view_wiki'] && Apps::system_app_installed($uid, 'Wiki')) {
$tabs[] = [
'label' => t('Wikis'),
'url' => z_root() . '/wiki/' . $nickname,
'sel' => ((argv(0) == 'wiki') ? 'active' : ''),
'title' => t('Wiki'),
'id' => 'wiki-tab',
'icon' => 'pencil-square-o'
];
}
$arr = array('is_owner' => $is_owner, 'nickname' => $nickname, 'tab' => (($tab) ? $tab : false), 'tabs' => $tabs);
Hook::call('channel_apps', $arr);
return replace_macros(
Theme::get_template('profile_tabs.tpl'),
[
'$tabs' => $arr['tabs'],
'$name' => App::$profile['channel_name'],
'$thumb' => App::$profile['thumb'],
]
);
}
}

41
Code/Lib/Nodeinfo.php Normal file
View file

@ -0,0 +1,41 @@
<?php
namespace Code\Lib;
class Nodeinfo
{
public static function fetch($url)
{
$href = EMPTY_STR;
$m = parse_url($url);
if ($m['scheme'] && $m['host']) {
$s = $m['scheme'] . '://' . $m['host'] . '/.well-known/nodeinfo';
$n = z_fetch_url($s);
if ($n['success']) {
$j = json_decode($n['body'], true);
if ($j && $j['links']) {
// lemmy just sends one result
if (isset($j['links']['rel'])) {
if ($j['links']['rel'] === 'http://nodeinfo.diaspora.software/ns/schema/2.0' && isset($j['links']['href'])) {
$href = $j['links']['href'];
}
} else {
foreach ($j['links'] as $l) {
if (isset($l['rel']) && $l['rel'] === 'http://nodeinfo.diaspora.software/ns/schema/2.0' && isset($l['href'])) {
$href = $l['href'];
}
}
}
}
}
}
if ($href) {
$n = z_fetch_url($href);
if ($n['success']) {
return json_decode($n['body'], true);
}
}
return [];
}
}

483
Code/Lib/Oembed.php Normal file
View file

@ -0,0 +1,483 @@
<?php
namespace Code\Lib;
use App;
use Code\Lib\Cache;
use Code\Extend\Hook;
use Code\Render\Theme;
class Oembed
{
public static function replacecb($matches)
{
$embedurl = $matches[1];
$result = self::action($embedurl);
if ($result['action'] === 'block') {
return '<a href="' . $result['url'] . '">' . $result['url'] . '</a>';
}
$j = self::fetch_url($result['url']);
$s = self::format_object($j);
return $s;
}
public static function action($embedurl)
{
$host = '';
$action = 'filter';
$embedurl = trim(str_replace('&amp;', '&', $embedurl));
//logger('oembed_action: ' . $embedurl, LOGGER_DEBUG, LOG_INFO);
if (strpos($embedurl, 'http://') === 0) {
if (intval(get_config('system', 'embed_sslonly'))) {
$action = 'block';
}
}
if (strpos($embedurl, '.well-known') !== false) {
$action = 'block';
}
// site allow/deny list
if (($x = get_config('system', 'embed_deny'))) {
if (($x) && (! is_array($x))) {
$x = explode("\n", $x);
}
if ($x) {
foreach ($x as $ll) {
$t = trim($ll);
if (($t) && (strpos($embedurl, $t) !== false)) {
$action = 'block';
break;
}
}
}
}
$found = false;
if (($x = get_config('system', 'embed_allow'))) {
if (($x) && (! is_array($x))) {
$x = explode("\n", $x);
}
if ($x) {
foreach ($x as $ll) {
$t = trim($ll);
if (($t) && (strpos($embedurl, $t) !== false) && ($action !== 'block')) {
$found = true;
$action = 'allow';
break;
}
}
}
if ((! $found) && ($action !== 'block')) {
$action = 'filter';
}
}
// allow individual members to block something that wasn't blocked already.
// They cannot over-ride the site to allow or change the filtering on an
// embed that is not allowed by the site admin.
if (local_channel()) {
if (($x = get_pconfig(local_channel(), 'system', 'embed_deny'))) {
if (($x) && (! is_array($x))) {
$x = explode("\n", $x);
}
if ($x) {
foreach ($x as $ll) {
$t = trim($ll);
if (($t) && (strpos($embedurl, $t) !== false)) {
$action = 'block';
break;
}
}
}
}
}
$arr = array('url' => $embedurl, 'action' => $action);
Hook::call('oembed_action', $arr);
//logger('action: ' . $arr['action'] . ' url: ' . $arr['url'], LOGGER_DEBUG,LOG_DEBUG);
return $arr;
}
// if the url is embeddable with oembed, return the bbcode link.
public static function process($url)
{
$j = self::fetch_url($url);
logger('oembed_process: ' . print_r($j, true), LOGGER_DATA, LOG_DEBUG);
if ($j && $j['type'] !== 'error') {
return '[embed]' . $url . '[/embed]';
}
return false;
}
public static function fetch_url($embedurl)
{
$noexts = [ '.mp3', '.mp4', '.ogg', '.ogv', '.oga', '.ogm', '.webm', '.opus', '.m4a', '.mov' ];
$result = self::action($embedurl);
$embedurl = $result['url'];
$action = $result['action'];
foreach ($noexts as $ext) {
if (strpos(strtolower($embedurl), $ext) !== false) {
$action = 'block';
}
}
$txt = null;
// we should try to cache this and avoid a lookup on each render
$is_matrix = is_matrix_url($embedurl);
$zrl = ((get_config('system', 'oembed_zrl')) ? $is_matrix : false);
$furl = ((local_channel() && $zrl) ? zid($embedurl) : $embedurl);
if ($action !== 'block' && (! get_config('system', 'oembed_cache_disable'))) {
$txt = Cache::get('[' . App::$videowidth . '] ' . $furl);
}
if (strpos(strtolower($embedurl), '.pdf') !== false && get_config('system', 'inline_pdf')) {
$action = 'allow';
$j = [
'html' => '<object data="' . $embedurl . '" type="application/pdf" style="width: 100%; height: 300px;"></object>',
'title' => t('View PDF'),
'type' => 'pdf'
];
// set $txt to something so that we don't attempt to fetch what could be a lengthy pdf.
$txt = EMPTY_STR;
}
if (is_null($txt)) {
$txt = "";
if ($action !== 'block') {
// try oembed autodiscovery
$redirects = 0;
$result = z_fetch_url(
$furl,
false,
$redirects,
[
'timeout' => 30,
'accept_content' => "text/*",
'novalidate' => true,
'session' => ((local_channel() && $zrl) ? true : false)
]
);
if ($result['success']) {
$html_text = $result['body'];
} else {
logger('fetch failure: ' . $furl);
}
if ($html_text) {
$dom = new DOMDocument();
@$dom->loadHTML($html_text);
if ($dom) {
$xpath = new DOMXPath($dom);
$attr = "oembed";
$xattr = self::build_xpath("class", "oembed");
$entries = $xpath->query("//link[@type='application/json+oembed']");
foreach ($entries as $e) {
$href = $e->getAttributeNode("href")->nodeValue;
$x = z_fetch_url($href . '&maxwidth=' . App::$videowidth);
if ($x['success']) {
$txt = $x['body'];
} else {
logger('fetch failed: ' . $href);
}
break;
}
// soundcloud is now using text/json+oembed instead of application/json+oembed,
// others may be also
$entries = $xpath->query("//link[@type='text/json+oembed']");
foreach ($entries as $e) {
$href = $e->getAttributeNode("href")->nodeValue;
$x = z_fetch_url($href . '&maxwidth=' . App::$videowidth);
if ($x['success']) {
$txt = $x['body'];
} else {
logger('json fetch failed: ' . $href);
}
break;
}
}
}
}
if ($txt == false || $txt == "") {
$x = array('url' => $embedurl,'videowidth' => App::$videowidth);
Hook::call('oembed_probe', $x);
if (array_key_exists('embed', $x)) {
$txt = $x['embed'];
}
}
$txt = trim($txt);
if ($txt[0] != "{") {
$txt = '{"type":"error"}';
}
// save in cache
if (! get_config('system', 'oembed_cache_disable')) {
Cache::set('[' . App::$videowidth . '] ' . $furl, $txt);
}
}
if (! $j) {
$j = json_decode($txt, true);
}
if (! $j) {
$j = [];
}
if ($action === 'filter') {
if ($j['html']) {
$orig = $j['html'];
$allow_position = (($is_matrix) ? true : false);
// some sites (e.g. Mastodon) wrap their entire embed in an iframe
// which we will purify away and which we provide anyway.
// So if we see this, grab the frame src url and use that
// as the embed content - which will still need to be purified.
if (preg_match('#\<iframe(.*?)src\=[\'\"](.*?)[\'\"]#', $j['html'], $matches)) {
$x = z_fetch_url($matches[2]);
$orig = $j['html'] = $x['body'];
}
// logger('frame src: ' . $j['html'], LOGGER_DATA);
$j['html'] = purify_html($j['html'], (($allow_position) ? [ 'allow_position' ] : []));
if ($j['html'] != $orig) {
// logger('oembed html was purified. original: ' . $orig . ' purified: ' . $j['html'], LOGGER_DEBUG, LOG_INFO);
}
$orig_len = mb_strlen(preg_replace('/\s+/', '', $orig));
$new_len = mb_strlen(preg_replace('/\s+/', '', $j['html']));
if (stripos($orig, '<script') || (! $new_len)) {
$j['type'] = 'error';
} elseif ($orig_len) {
$ratio = $new_len / $orig_len;
if ($ratio < 0.5) {
$j['type'] = 'error';
logger('oembed html truncated: ' . $ratio, LOGGER_DEBUG, LOG_INFO);
}
}
}
}
$j['embedurl'] = $embedurl;
$j['zrl'] = $is_matrix;
// logger('fetch return: ' . print_r($j,true));
return $j;
}
public static function format_object($j)
{
$embedurl = $j['embedurl'];
// logger('format: ' . print_r($j,true));
$jhtml = self::iframe($j['embedurl'], (isset($j['width']) ? $j['width'] : null), (isset($j['height']) ? $j['height'] : null));
$ret = "<span class='oembed " . $j['type'] . "'>";
switch ($j['type']) {
case "video": {
if (isset($j['thumbnail_url'])) {
$tw = (isset($j['thumbnail_width'])) ? $j['thumbnail_width'] : 200;
$th = (isset($j['thumbnail_height'])) ? $j['thumbnail_height'] : 180;
$tr = $tw / $th;
$th = 120;
$tw = $th * $tr;
$tpl = Theme::get_template('oembed_video.tpl');
$ret .= replace_macros($tpl, array(
'$baseurl' => z_root(),
'$embedurl' => $embedurl,
'$escapedhtml' => base64_encode($jhtml),
'$tw' => $tw,
'$th' => $th,
'$turl' => $j['thumbnail_url'],
));
} else {
$ret = $jhtml;
}
$ret .= "<br>";
}
break;
case "photo": {
$ret .= "<img width='" . $j['width'] . "' src='" . $j['url'] . "'>";
$ret .= "<br>";
}
break;
case "link": {
if ($j['thumbnail_url']) {
if (is_matrix_url($embedurl)) {
$embedurl = zid($embedurl);
$j['thumbnail_url'] = zid($j['thumbnail_url']);
}
$ret = '<a href="' . $embedurl . '" ><img src="' . $j['thumbnail_url'] . '" alt="thumbnail" /></a><br><br>';
}
//$ret = "<a href='".$embedurl."'>".$j['title']."</a>";
}
break;
case 'pdf': {
$ret = $j['html'];
break;
}
case "rich":
if ($j['zrl']) {
$ret = ((preg_match('/^<div[^>]+>(.*?)<\/div>$/is', $j['html'], $o)) ? $o[1] : $j['html']);
} else {
$ret .= $jhtml;
}
break;
}
// add link to source if not present in "rich" type
if ($j['type'] != 'rich' || !strpos($j['html'], $embedurl)) {
$embedlink = (isset($j['title'])) ? $j['title'] : $embedurl;
$ret .= '<br>' . "<a href='$embedurl' rel='oembed'>$embedlink</a>";
$ret .= "<br>";
if (isset($j['author_name'])) {
$ret .= t(' by ') . $j['author_name'];
}
if (isset($j['provider_name'])) {
$ret .= t(' on ') . $j['provider_name'];
}
} else {
// add <a> for html2bbcode conversion
$ret .= "<br><a href='$embedurl' rel='oembed'>$embedurl</a>";
}
$ret .= "<br style='clear:left'></span>";
return mb_convert_encoding($ret, 'HTML-ENTITIES', mb_detect_encoding($ret));
}
public static function iframe($src, $width, $height)
{
$scroll = ' scrolling="no" ';
if (! $width || strstr($width, '%')) {
$width = '640';
$scroll = ' scrolling="auto" ';
}
if (! $height || strstr($height, '%')) {
$height = '300';
$scroll = ' scrolling="auto" ';
}
// try and leave some room for the description line.
$height = intval($height) + 80;
$width = intval($width) + 40;
$s = z_root() . '/oembed/' . base64url_encode($src);
// Make sure any children are sandboxed within their own iframe.
return '<iframe ' . ' style="max-width: 100%;" ' . $scroll . 'height="' . $height . '" width="' . $width . '" src="' . $s . '" allowfullscreen frameborder="no" >'
. t('Embedded content') . '</iframe>';
}
public static function bbcode2html($text)
{
$stopoembed = get_config("system", "no_oembed");
if ($stopoembed == true) {
return preg_replace("/\[embed\](.+?)\[\/embed\]/is", "<!-- oembed $1 --><i>" . t('Embedding disabled') . " : $1</i><!-- /oembed $1 -->", $text);
}
return preg_replace_callback("/\[embed\](.+?)\[\/embed\]/is", ['\\Code\\Lib\\Oembed','replacecb'], $text);
}
public static function build_xpath($attr, $value)
{
// http://westhoffswelt.de/blog/0036_xpath_to_select_html_by_class.html
return "contains( normalize-space( @$attr ), ' $value ' ) or substring( normalize-space( @$attr ), 1, string-length( '$value' ) + 1 ) = '$value ' or substring( normalize-space( @$attr ), string-length( @$attr ) - string-length( '$value' ) ) = ' $value' or @$attr = '$value'";
}
public static function get_inner_html($node)
{
$innerHTML = '';
$children = $node->childNodes;
foreach ($children as $child) {
$innerHTML .= $child->ownerDocument->saveXML($child);
}
return $innerHTML;
}
/**
* Find <span class='oembed'>..<a href='url' rel='oembed'>..</a></span>
* and replace it with [embed]url[/embed]
*/
public static function html2bbcode($text)
{
// start parser only if 'oembed' is in text
if (strpos($text, "oembed")) {
// convert non ascii chars to html entities
$html_text = mb_convert_encoding($text, 'HTML-ENTITIES', mb_detect_encoding($text));
// If it doesn't parse at all, just return the text.
$dom = new DOMDocument();
@$dom->loadHTML($html_text);
if ($dom) {
$xpath = new DOMXPath($dom);
$attr = "oembed";
$xattr = self::build_xpath("class", "oembed");
$entries = $xpath->query("//span[$xattr]");
$xattr = "@rel='oembed'";//self::build_xpath("rel","oembed");
foreach ($entries as $e) {
$href = $xpath->evaluate("a[$xattr]/@href", $e)->item(0)->nodeValue;
if (!is_null($href)) {
$e->parentNode->replaceChild(new DOMText("[embed]" . $href . "[/embed]"), $e);
}
}
return self::get_inner_html($dom->getElementsByTagName("body")->item(0));
}
}
return $text;
}
}

229
Code/Lib/PConfig.php Normal file
View file

@ -0,0 +1,229 @@
<?php
namespace Code\Lib;
use App;
/**
* @brief Class for handling channel specific configurations.
*
* <b>PConfig</b> is used for channel specific configurations and takes a
* <i>channel_id</i> as identifier. It stores for example which features are
* enabled per channel. The storage is of size MEDIUMTEXT.
*
* @code{.php}$var = Code\Lib\PConfig::Get('uid', 'category', 'key');
* // with default value for non existent key
* $var = Code\Lib\PConfig::Get('uid', 'category', 'unsetkey', 'defaultvalue');@endcode
*
* The old (deprecated?) way to access a PConfig value is:
* @code{.php}$var = get_pconfig(local_channel(), 'category', 'key');@endcode
*/
class PConfig
{
/**
* @brief Loads all configuration values of a channel into a cached storage.
*
* All configuration values of the given channel are stored in global cache
* which is available under the global variable App::$config[$uid].
*
* @param string $uid
* The channel_id
* @return void|false Nothing or false if $uid is null or false
*/
public static function Load($uid)
{
if (is_null($uid) || $uid === false) {
return false;
}
if (! is_array(App::$config)) {
btlogger('App::$config not an array');
}
if (! array_key_exists($uid, App::$config)) {
App::$config[$uid] = [];
}
if (! is_array(App::$config[$uid])) {
btlogger('App::$config[$uid] not an array: ' . $uid);
}
$r = q(
"SELECT * FROM pconfig WHERE uid = %d",
intval($uid)
);
if ($r) {
foreach ($r as $rr) {
$k = $rr['k'];
$c = $rr['cat'];
if (! array_key_exists($c, App::$config[$uid])) {
App::$config[$uid][$c] = [];
App::$config[$uid][$c]['config_loaded'] = true;
}
App::$config[$uid][$c][$k] = $rr['v'];
}
}
}
/**
* @brief Get a particular channel's config variable given the category name
* ($family) and a key.
*
* Get a particular channel's config value from the given category ($family)
* and the $key from a cached storage in App::$config[$uid].
*
* Returns false if not set.
*
* @param string $uid
* The channel_id
* @param string $family
* The category of the configuration value
* @param string $key
* The configuration key to query
* @param mixed $default (optional, default false)
* Default value to return if key does not exist
* @return mixed Stored value or false if it does not exist
*/
public static function Get($uid, $family, $key, $default = false)
{
if (is_null($uid) || $uid === false) {
return $default;
}
if (! array_key_exists($uid, App::$config)) {
self::Load($uid);
}
if ((! array_key_exists($family, App::$config[$uid])) || (! array_key_exists($key, App::$config[$uid][$family]))) {
return $default;
}
return unserialise(App::$config[$uid][$family][$key]);
}
/**
* @brief Sets a configuration value for a channel.
*
* Stores a config value ($value) in the category ($family) under the key ($key)
* for the channel_id $uid.
*
* @param string $uid
* The channel_id
* @param string $family
* The category of the configuration value
* @param string $key
* The configuration key to set
* @param string $value
* The value to store
* @return mixed Stored $value or false
*/
public static function Set($uid, $family, $key, $value)
{
// this catches subtle errors where this function has been called
// with local_channel() when not logged in (which returns false)
// and throws an error in array_key_exists below.
// we provide a function backtrace in the logs so that we can find
// and fix the calling function.
if (is_null($uid) || $uid === false) {
btlogger('UID is FALSE!', LOGGER_NORMAL, LOG_ERR);
return;
}
// manage array value
$dbvalue = ((is_array($value)) ? serialise($value) : $value);
$dbvalue = ((is_bool($dbvalue)) ? intval($dbvalue) : $dbvalue);
if (self::Get($uid, $family, $key) === false) {
if (! array_key_exists($uid, App::$config)) {
App::$config[$uid] = [];
}
if (! array_key_exists($family, App::$config[$uid])) {
App::$config[$uid][$family] = [];
}
$ret = q(
"INSERT INTO pconfig ( uid, cat, k, v ) VALUES ( %d, '%s', '%s', '%s' ) ",
intval($uid),
dbesc($family),
dbesc($key),
dbesc($dbvalue)
);
} else {
$ret = q(
"UPDATE pconfig SET v = '%s' WHERE uid = %d and cat = '%s' AND k = '%s'",
dbesc($dbvalue),
intval($uid),
dbesc($family),
dbesc($key)
);
}
// keep a separate copy for all variables which were
// set in the life of this page. We need this to
// synchronise channel clones.
if (! array_key_exists('transient', App::$config[$uid])) {
App::$config[$uid]['transient'] = [];
}
if (! array_key_exists($family, App::$config[$uid]['transient'])) {
App::$config[$uid]['transient'][$family] = [];
}
App::$config[$uid][$family][$key] = $value;
App::$config[$uid]['transient'][$family][$key] = $value;
if ($ret) {
return $value;
}
return $ret;
}
/**
* @brief Deletes the given key from the channel's configuration.
*
* Removes the configured value from the stored cache in App::$config[$uid]
* and removes it from the database.
*
* @param string $uid
* The channel_id
* @param string $family
* The category of the configuration value
* @param string $key
* The configuration key to delete
* @return mixed
*/
public static function Delete($uid, $family, $key)
{
if (is_null($uid) || $uid === false) {
return false;
}
$ret = false;
if (
array_key_exists($uid, App::$config)
&& is_array(App::$config['uid'])
&& array_key_exists($family, App::$config['uid'])
&& array_key_exists($key, App::$config[$uid][$family])
) {
unset(App::$config[$uid][$family][$key]);
}
$ret = q(
"DELETE FROM pconfig WHERE uid = %d AND cat = '%s' AND k = '%s'",
intval($uid),
dbesc($family),
dbesc($key)
);
return $ret;
}
}

226
Code/Lib/Permcat.php Normal file
View file

@ -0,0 +1,226 @@
<?php
namespace Code\Lib;
use Code\Access\PermissionRoles;
use Code\Access\Permissions;
use Code\Lib\Channel;
use Code\Extend\Hook;
/**
* @brief Permission Categories. Permission rules for various classes of connections.
*
* Connection permissions answer the question "Can Joe view my photos?"
*
* Some permissions may be inherited from the channel's "privacy settings"
* (@ref ::Code::Access::PermissionLimits "PermissionLimits") "Who can view my
* photos (at all)?" which have higher priority than individual connection settings.
* We evaluate permission limits first, and then fall through to connection
* permissions if the permission limits didn't already make a definitive decision.
*
* After PermissionLimits and connection permissions are evaluated, individual
* content ACLs are evaluated (@ref ::Code::Access::AccessList "AccessList").
* These answer the question "Can Joe view *this* album/photo?".
*/
class Permcat
{
/**
* @var array
*/
private $permcats = [];
/**
* @brief Permcat constructor.
*
* @param int $channel_id
*/
public function __construct($channel_id, $abook_id = 0)
{
$perms = [];
// first check role perms for a perms_connect setting
$role = get_pconfig($channel_id, 'system', 'permissions_role');
if ($role) {
$x = PermissionRoles::role_perms($role);
if ($x['perms_connect']) {
$perms = Permissions::FilledPerms($x['perms_connect']);
}
}
// if no role perms it may be a custom role, see if there any autoperms
if (! $perms) {
$perms = Permissions::FilledAutoperms($channel_id);
}
// if no autoperms it may be a custom role with manual perms
if (! $perms) {
$c = Channel::from_id($channel_id);
if ($c) {
$perms = Permissions::FilledPerms(explode(',',get_abconfig($channel_id, $c['channel_hash'], 'system', 'my_perms', EMPTY_STR)));
}
}
// nothing was found - create a filled permission array where all permissions are 0
if (! $perms) {
$perms = Permissions::FilledPerms([]);
}
$this->permcats[] = [
'name' => 'default',
'localname' => t('default', 'permcat'),
'perms' => Permissions::Operms($perms),
'system' => 1
];
$p = $this->load_permcats($channel_id, $abook_id);
if ($p) {
for ($x = 0; $x < count($p); $x++) {
$this->permcats[] = [
'name' => $p[$x][0],
'localname' => $p[$x][1],
'perms' => Permissions::Operms(Permissions::FilledPerms($p[$x][2])),
'system' => intval($p[$x][3])
];
}
}
}
public function match($current) {
if ($current) {
$perms = Permissions::FilledPerms($current);
$operms = Permissions::Operms($perms);
}
if ($this->permcats && $operms) {
foreach($this->permcats as $permcat) {
$pp = $permcat['perms'];
$matching = 0;
foreach ($pp as $rp) {
foreach ($operms as $op) {
if ($rp['name'] === $op['name'] && intval($rp['value']) === intval($op['value'])) {
$matching ++;
break;
}
}
}
if ($matching === count($pp)) {
return $permcat['name'];
}
}
}
return 'custom';
}
/**
* @brief Return array with permcats.
*
* @return array
*/
public function listing()
{
return $this->permcats;
}
/**
* @brief
*
* @param string $name
* @return array
* * \e array with permcats
* * \e bool \b error if $name not found in permcats true
*/
public function fetch($name)
{
if ($name && $this->permcats) {
foreach ($this->permcats as $permcat) {
if (strcasecmp($permcat['name'], $name) === 0) {
return $permcat;
}
}
}
return ['error' => true];
}
public function load_permcats($uid, $abook_id = 0)
{
$permcats = [
[ 'follower', t('follower', 'permcat'),
[ 'view_stream','view_profile','view_contacts','view_storage','view_pages','view_wiki',
'post_like' ], 1
],
[ 'contributor', t('contributor', 'permcat'),
[ 'view_stream','view_profile','view_contacts','view_storage','view_pages','view_wiki',
'post_wall','post_comments','write_wiki','post_like','tag_deliver','chat' ], 1
],
[ 'publisher', t('publisher', 'permcat'),
[ 'view_stream','view_profile','view_contacts','view_storage','view_pages',
'write_storage','post_wall','write_pages','write_wiki','post_comments','post_like','tag_deliver',
'chat', 'republish' ], 1
]
];
if ($uid) {
$x = q(
"select * from pconfig where uid = %d and cat = 'permcat'",
intval($uid)
);
if ($x) {
foreach ($x as $xv) {
$value = unserialise($xv['v']);
$permcats[] = [ $xv['k'], $xv['k'], $value, 0 ];
}
}
}
if ($abook_id) {
$r = q("select * from abook left join xchan on abook_xchan = xchan_hash where abook_id = %d and abook_channel = %d",
intval($abook_id),
intval($uid)
);
if ($r) {
$my_perms = explode(',', get_abconfig($uid, $r[0]['xchan_hash'], 'system', 'my_perms', EMPTY_STR));
$permcats[] = [ 'custom', t('custom'), $my_perms, 1];
}
}
/**
* @hooks permcats
* * \e array
*/
Hook::call('permcats', $permcats);
return $permcats;
}
public static function find_permcat($arr, $name)
{
if ((! $arr) || (! $name)) {
return false;
}
foreach ($arr as $p) {
if ($p['name'] == $name) {
return $p['value'];
}
}
}
public static function update($channel_id, $name, $permarr)
{
PConfig::Set($channel_id, 'permcat', $name, $permarr);
}
public static function delete($channel_id, $name)
{
PConfig::Delete($channel_id, 'permcat', $name);
}
}

View file

@ -0,0 +1,193 @@
<?php
namespace Code\Lib;
use App;
use Code\Access\PermissionLimits;
use Code\Access\Permissions;
require_once("include/permissions.php");
require_once("include/language.php");
require_once("include/text.php");
/**
* Encapsulates information the ACL dialog requires to describe
* permission settings for an item with an empty ACL.
* i.e the caption, icon, and tooltip for the no-ACL option in the ACL dialog.
*/
class PermissionDescription
{
private $global_perm;
private $channel_perm;
private $fallback_description;
/**
* Constructor is private.
* Use static methods fromGlobalPermission(), fromStandalonePermission(),
* or fromDescription() to create instances.
*
* @internal
* @param int $global_perm
* @param int $channel_perm
* @param string $description (optional) default empty
*/
private function __construct($global_perm, $channel_perm, $description = '')
{
$this->global_perm = $global_perm;
$this->channel_perm = $channel_perm;
$this->fallback_description = ($description == '') ? t('Visible to your default audience') : $description;
}
/**
* If the interpretation of an empty ACL can't be summarised with a global default permission
* or a specific permission setting then use this method and describe what it means instead.
* Remember to localize the description first.
*
* @param string $description - the localized caption for the no-ACL option in the ACL dialog.
* @return a new instance of PermissionDescription
*/
public static function fromDescription($description)
{
return new PermissionDescription('', 0x80000, $description);
}
/**
* Use this method only if the interpretation of an empty ACL doesn't fall back to a global
* default permission. You should pass one of the constants from boot.php - PERMS_PUBLIC,
* PERMS_NETWORK etc.
*
* @param int $perm - a single enumerated constant permission - PERMS_PUBLIC, PERMS_NETWORK etc.
* @return a new instance of PermissionDescription
*/
public static function fromStandalonePermission($perm)
{
$result = new PermissionDescription('', $perm);
$checkPerm = $result->get_permission_description();
if ($checkPerm == $result->fallback_description) {
$result = null;
logger('null PermissionDescription from unknown standalone permission: ' . $perm, LOGGER_DEBUG, LOG_ERR);
}
return $result;
}
/**
* This is the preferred way to create a PermissionDescription, as it provides the most details.
* Use this method if you know an empty ACL will result in one of the global default permissions
* being used, such as channel_r_stream (for which you would pass 'view_stream').
*
* @param string $permname - a key for the global perms array from get_perms() in permissions.php,
* e.g. 'view_stream', 'view_profile', etc.
* @return a new instance of PermissionDescription
*/
public static function fromGlobalPermission($permname)
{
$result = null;
$global_perms = Permissions::Perms();
if (array_key_exists($permname, $global_perms)) {
$channelPerm = PermissionLimits::Get(App::$channel['channel_id'], $permname);
$result = new PermissionDescription('', $channelPerm);
} else {
// The acl dialog can handle null arguments, but it shouldn't happen
logger('null PermissionDescription from unknown global permission: ' . $permname, LOGGER_DEBUG, LOG_ERR);
}
return $result;
}
/**
* Gets a localized description of the permission, or a generic message if the permission
* is unknown.
*
* @return string description
*/
public function get_permission_description()
{
switch ($this->channel_perm) {
case 0:
return t('Only me');
case PERMS_PUBLIC:
return t('Public');
case PERMS_NETWORK:
return t('Anybody in the $Projectname network');
case PERMS_SITE:
return sprintf(t('Any account on %s'), App::get_hostname());
case PERMS_CONTACTS:
return t('Any of my connections');
case PERMS_SPECIFIC:
return t('Only connections I specifically allow');
case PERMS_AUTHED:
return t('Anybody authenticated (could include visitors from other networks)');
case PERMS_PENDING:
return t('Any connections including those who haven\'t yet been approved');
default:
return $this->fallback_description;
}
}
/**
* Returns an icon css class name if an appropriate one is available, e.g. "fa-globe" for Public,
* otherwise returns empty string.
*
* @return string icon css class name (often FontAwesome)
*/
public function get_permission_icon()
{
switch ($this->channel_perm) {
case 0:
return 'fa-eye-slash';
case PERMS_PUBLIC:
return 'fa-globe';
case PERMS_NETWORK:
return 'fa-share-alt-square'; // fa-share-alt-square is very similiar to the hubzilla logo, but we should create our own logo class to use
case PERMS_SITE:
return 'fa-sitemap';
case PERMS_CONTACTS:
return 'fa-group';
case PERMS_SPECIFIC:
return 'fa-list';
case PERMS_AUTHED:
return '';
case PERMS_PENDING:
return '';
default:
return '';
}
}
/**
* Returns a localized description of where the permission came from, if this is known.
* If it's not know, or if the permission is standalone and didn't come from a default
* permission setting, then empty string is returned.
*
* @return string description or empty string
*/
public function get_permission_origin_description()
{
switch ($this->global_perm) {
case PERMS_R_STREAM:
return t('This is your default setting for the audience of your normal stream, and posts.');
case PERMS_R_PROFILE:
return t('This is your default setting for who can view your default channel profile');
case PERMS_R_ABOOK:
return t('This is your default setting for who can view your connections');
case PERMS_R_STORAGE:
return t('This is your default setting for who can view your file storage and photos');
case PERMS_R_PAGES:
return t('This is your default setting for the audience of your webpages');
default:
return '';
}
}
}

522
Code/Lib/Queue.php Normal file
View file

@ -0,0 +1,522 @@
<?php
/** @file */
namespace Code\Lib;
use Code\Lib\Libzot;
use Code\Web\HTTPSig;
use Code\Lib\Activity;
use Code\Lib\ActivityStreams;
use Code\Lib\Channel;
use Code\Zot6\Receiver;
use Code\Zot6\Zot6Handler;
use Code\Extend\Hook;
class Queue
{
public static function update($id, $add_priority = 0)
{
logger('queue: requeue item ' . $id, LOGGER_DEBUG);
// This queue item failed. Perhaps it was rejected. Perhaps the site is dead.
// Since we don't really know, check and see if we've got something else destined
// for that server and give it priority. At a minimum it will keep the queue from
// getting stuck on a particular message when another one with different content
// might actually succeed.
$x = q(
"select outq_created, outq_hash, outq_posturl from outq where outq_hash = '%s' limit 1",
dbesc($id)
);
if (!$x) {
return;
}
$g = q(
"select outq_created, outq_hash, outq_posturl from outq where outq_posturl = '%s' and outq_hash != '%s' limit 1",
dbesc($x[0]['outq_posturl']),
dbesc($id)
);
// swap them
if ($g) {
$x = $g;
}
$y = q(
"select min(outq_created) as earliest from outq where outq_posturl = '%s'",
dbesc($x[0]['outq_posturl'])
);
// look for the oldest queue entry with this destination URL. If it's older than a couple of days,
// the destination is considered to be down and only scheduled once an hour, regardless of the
// age of the current queue item.
$might_be_down = false;
if ($y) {
$might_be_down = ((datetime_convert('UTC', 'UTC', $y[0]['earliest']) < datetime_convert('UTC', 'UTC', 'now - 2 days')) ? true : false);
}
// Set all other records for this destination way into the future.
// The queue delivers by destination. We'll keep one queue item for
// this destination (this one) with a shorter delivery. If we succeed
// once, we'll try to deliver everything for that destination.
// The delivery will be set to at most once per hour, and if the
// queue item is less than 12 hours old, we'll schedule for fifteen
// minutes.
$r = q(
"UPDATE outq SET outq_scheduled = '%s' WHERE outq_posturl = '%s'",
dbesc(datetime_convert('UTC', 'UTC', 'now + 5 days')),
dbesc($x[0]['outq_posturl'])
);
$since = datetime_convert('UTC', 'UTC', $y[0]['earliest']);
if (($might_be_down) || ($since < datetime_convert('UTC', 'UTC', 'now - 12 hour'))) {
$next = datetime_convert('UTC', 'UTC', 'now + 1 hour');
} else {
$next = datetime_convert('UTC', 'UTC', 'now + ' . intval($add_priority) . ' minutes');
}
q(
"UPDATE outq SET outq_updated = '%s',
outq_priority = outq_priority + %d,
outq_scheduled = '%s'
WHERE outq_hash = '%s'",
dbesc(datetime_convert()),
intval($add_priority),
dbesc($next),
dbesc($x[0]['outq_hash'])
);
}
public static function remove($id, $channel_id = 0)
{
logger('queue: remove queue item ' . $id, LOGGER_DEBUG);
$sql_extra = (($channel_id) ? " and outq_channel = " . intval($channel_id) . " " : '');
q(
"DELETE FROM outq WHERE outq_hash = '%s' $sql_extra",
dbesc($id)
);
}
public static function remove_by_posturl($posturl)
{
logger('queue: remove queue posturl ' . $posturl, LOGGER_DEBUG);
q(
"DELETE FROM outq WHERE outq_posturl = '%s' ",
dbesc($posturl)
);
}
public static function set_delivered($id, $channel = 0)
{
logger('queue: set delivered ' . $id, LOGGER_DEBUG);
$sql_extra = (($channel_id) ? " and outq_channel = " . intval($channel_id) . " " : '');
// Set the next scheduled run date so far in the future that it will be expired
// long before it ever makes it back into the delivery chain.
q(
"update outq set outq_delivered = 1, outq_updated = '%s', outq_scheduled = '%s' where outq_hash = '%s' $sql_extra ",
dbesc(datetime_convert()),
dbesc(datetime_convert('UTC', 'UTC', 'now + 5 days')),
dbesc($id)
);
}
public static function insert($arr)
{
logger('insert: ' . print_r($arr, true), LOGGER_DATA);
// do not queue anything with no destination
if (!(array_key_exists('posturl', $arr) && trim($arr['posturl']))) {
logger('no destination');
return false;
}
$x = q(
"insert into outq ( outq_hash, outq_account, outq_channel, outq_driver, outq_posturl, outq_async, outq_priority,
outq_created, outq_updated, outq_scheduled, outq_notify, outq_msg )
values ( '%s', %d, %d, '%s', '%s', %d, %d, '%s', '%s', '%s', '%s', '%s' )",
dbesc($arr['hash']),
intval($arr['account_id']),
intval($arr['channel_id']),
dbesc((isset($arr['driver']) && $arr['driver']) ? $arr['driver'] : 'nomad'),
dbesc($arr['posturl']),
intval(1),
intval((isset($arr['priority'])) ? $arr['priority'] : 0),
dbesc(datetime_convert()),
dbesc(datetime_convert()),
dbesc((isset($arr['scheduled'])) ? $arr['scheduled'] : datetime_convert()),
dbesc($arr['notify']),
dbesc(($arr['msg']) ? $arr['msg'] : '')
);
return $x;
}
public static function deliver($outq, $immediate = false)
{
$base = null;
$h = parse_url($outq['outq_posturl']);
if ($h !== false) {
$base = $h['scheme'] . '://' . $h['host'] . ((isset($h['port']) && intval($h['port'])) ? ':' . $h['port'] : '');
}
if (($base) && ($base !== z_root()) && ($immediate)) {
$y = q(
"select site_update, site_dead from site where site_url = '%s' ",
dbesc($base)
);
if ($y) {
if (intval($y[0]['site_dead'])) {
q(
"update dreport set dreport_result = '%s' where dreport_queue = '%s'",
dbesc('site dead'),
dbesc($outq['outq_hash'])
);
self::remove_by_posturl($outq['outq_posturl']);
logger('dead site ignored ' . $base);
return;
}
if ($y[0]['site_update'] < datetime_convert('UTC', 'UTC', 'now - 1 month')) {
q(
"update dreport set dreport_log = '%s' where dreport_queue = '%s'",
dbesc('site deferred'),
dbesc($outq['outq_hash'])
);
self::update($outq['outq_hash'], 10);
logger('immediate delivery deferred for site ' . $base);
return;
}
} else {
// zot sites should all have a site record, unless they've been dead for as long as
// your site has existed. Since we don't know for sure what these sites are,
// call them unknown
site_store_lowlevel(
[
'site_url' => $base,
'site_update' => datetime_convert(),
'site_dead' => 0,
'site_type' => ((in_array($outq['outq_driver'], ['post', 'activitypub'])) ? SITE_TYPE_NOTZOT : SITE_TYPE_UNKNOWN),
'site_crypto' => ''
]
);
}
}
$arr = array('outq' => $outq, 'base' => $base, 'handled' => false, 'immediate' => $immediate);
Hook::call('queue_deliver', $arr);
if ($arr['handled']) {
return;
}
// "post" queue driver - used for diaspora and friendica-over-diaspora communications.
if ($outq['outq_driver'] === 'post') {
$result = z_post_url($outq['outq_posturl'], $outq['outq_msg']);
if ($result['success'] && $result['return_code'] < 300) {
logger('deliver: queue post success to ' . $outq['outq_posturl'], LOGGER_DEBUG);
if ($base) {
q(
"update site set site_update = '%s', site_dead = 0 where site_url = '%s' ",
dbesc(datetime_convert()),
dbesc($base)
);
}
q(
"update dreport set dreport_result = '%s', dreport_time = '%s' where dreport_queue = '%s'",
dbesc('accepted for delivery'),
dbesc(datetime_convert()),
dbesc($outq['outq_hash'])
);
self::remove($outq['outq_hash']);
// server is responding - see if anything else is going to this destination and is piled up
// and try to send some more. We're relying on the fact that do_delivery() results in an
// immediate delivery otherwise we could get into a queue loop.
if (!$immediate) {
$x = q(
"select outq_hash from outq where outq_posturl = '%s' and outq_delivered = 0",
dbesc($outq['outq_posturl'])
);
$piled_up = [];
if ($x) {
foreach ($x as $xx) {
$piled_up[] = $xx['outq_hash'];
}
}
if ($piled_up) {
// call do_delivery() with the force flag
do_delivery($piled_up, true);
}
}
} else {
logger('deliver: queue post returned ' . $result['return_code']
. ' from ' . $outq['outq_posturl'], LOGGER_DEBUG);
self::update($outq['outq_hash'], 10);
}
return;
}
if ($outq['outq_driver'] === 'asfetch') {
$channel = Channel::from_id($outq['outq_channel']);
if (!$channel) {
logger('missing channel: ' . $outq['outq_channel']);
return;
}
if (!ActivityStreams::is_url($outq['outq_posturl'])) {
logger('fetch item is not url: ' . $outq['outq_posturl']);
self::remove($outq['outq_hash']);
return;
}
$j = Activity::fetch($outq['outq_posturl'], $channel);
if ($j) {
$AS = new ActivityStreams($j, null, true);
if ($AS->is_valid() && isset($AS->data['type'])) {
if (ActivityStreams::is_an_actor($AS->data['type'])) {
Activity::actor_store($AS->data['id'], $AS->data);
}
if (strpos($AS->data['type'], 'Collection') !== false) {
// we are probably fetching a collection already - and do not support collection recursion at this time
self::remove($outq['outq_hash']);
return;
}
$item = Activity::decode_note($AS, true);
if ($item) {
Activity::store($channel, $channel['channnel_hash'], $AS, $item, true, true);
}
}
logger('deliver: queue fetch success from ' . $outq['outq_posturl'], LOGGER_DEBUG);
self::remove($outq['outq_hash']);
// server is responding - see if anything else is going to this destination and is piled up
// and try to send some more. We're relying on the fact that do_delivery() results in an
// immediate delivery otherwise we could get into a queue loop.
if (!$immediate) {
$x = q(
"select outq_hash from outq where outq_driver = 'asfetch' and outq_channel = %d and outq_delivered = 0",
dbesc($outq['outq_channel'])
);
$piled_up = [];
if ($x) {
foreach ($x as $xx) {
$piled_up[] = $xx['outq_hash'];
}
}
if ($piled_up) {
do_delivery($piled_up, true);
}
}
} else {
logger('deliver: queue fetch failed' . ' from ' . $outq['outq_posturl'], LOGGER_DEBUG);
self::update($outq['outq_hash'], 10);
}
return;
}
if ($outq['outq_driver'] === 'activitypub') {
$channel = Channel::from_id($outq['outq_channel']);
if (!$channel) {
logger('missing channel: ' . $outq['outq_channel']);
return;
}
$retries = 0;
$m = parse_url($outq['outq_posturl']);
$headers = [];
$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['Host'] = $m['host'];
$headers['(request-target)'] = 'post ' . get_request_string($outq['outq_posturl']);
$xhead = HTTPSig::create_sig($headers, $channel['channel_prvkey'], Channel::url($channel));
if (strpos($outq['outq_posturl'], 'http') !== 0) {
logger('bad url: ' . $outq['outq_posturl']);
self::remove($outq['outq_hash']);
}
$result = z_post_url($outq['outq_posturl'], $outq['outq_msg'], $retries, ['headers' => $xhead]);
if ($result['success'] && $result['return_code'] < 300) {
logger('deliver: queue post success to ' . $outq['outq_posturl'], LOGGER_DEBUG);
if ($base) {
q(
"update site set site_update = '%s', site_dead = 0 where site_url = '%s' ",
dbesc(datetime_convert()),
dbesc($base)
);
}
q(
"update dreport set dreport_result = '%s', dreport_time = '%s' where dreport_queue = '%s'",
dbesc('accepted for delivery'),
dbesc(datetime_convert()),
dbesc($outq['outq_hash'])
);
self::remove($outq['outq_hash']);
// server is responding - see if anything else is going to this destination and is piled up
// and try to send some more. We're relying on the fact that do_delivery() results in an
// immediate delivery otherwise we could get into a queue loop.
if (!$immediate) {
$x = q(
"select outq_hash from outq where outq_posturl = '%s' and outq_delivered = 0",
dbesc($outq['outq_posturl'])
);
$piled_up = [];
if ($x) {
foreach ($x as $xx) {
$piled_up[] = $xx['outq_hash'];
}
}
if ($piled_up) {
do_delivery($piled_up, true);
}
}
}
elseif ($result['return_code'] >= 400 && $result['return_code'] < 500) {
q(
"update dreport set dreport_result = '%s', dreport_time = '%s' where dreport_queue = '%s'",
dbesc('delivery rejected' . ' ' . $result['return_code']),
dbesc(datetime_convert()),
dbesc($outq['outq_hash'])
);
self::remove($outq['outq_hash']);
}
else {
$dr = q(
"select * from dreport where dreport_queue = '%s'",
dbesc($outq['outq_hash'])
);
if ($dr) {
// update every queue entry going to this site with the most recent communication error
q(
"update dreport set dreport_log = '%s' where dreport_site = '%s'",
dbesc(z_curl_error($result)),
dbesc($dr[0]['dreport_site'])
);
}
self::update($outq['outq_hash'], 10);
}
logger('deliver: queue post returned ' . $result['return_code'] . ' from ' . $outq['outq_posturl'], LOGGER_DEBUG);
return;
}
// normal zot delivery
logger('deliver: dest: ' . $outq['outq_posturl'], LOGGER_DEBUG);
if ($outq['outq_posturl'] === z_root() . '/zot') {
// local delivery
$zot = new Receiver(new Zot6Handler(), $outq['outq_notify']);
$result = $zot->run();
logger('returned_json: ' . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), LOGGER_DATA);
logger('deliver: local zot delivery succeeded to ' . $outq['outq_posturl']);
Libzot::process_response($outq['outq_posturl'], ['success' => true, 'body' => json_encode($result)], $outq);
if (!$immediate) {
$x = q(
"select outq_hash from outq where outq_posturl = '%s' and outq_delivered = 0",
dbesc($outq['outq_posturl'])
);
$piled_up = [];
if ($x) {
foreach ($x as $xx) {
$piled_up[] = $xx['outq_hash'];
}
}
if ($piled_up) {
do_delivery($piled_up, true);
}
}
} else {
logger('remote');
$channel = null;
if ($outq['outq_channel']) {
$channel = Channel::from_id($outq['outq_channel'], true);
}
$host_crypto = null;
if ($channel && $base) {
$h = q(
"select hubloc_sitekey, site_crypto from hubloc left join site on hubloc_url = site_url where site_url = '%s' and hubloc_network in ('zot6','nomad') order by hubloc_id desc limit 1",
dbesc($base)
);
if ($h) {
$host_crypto = $h[0];
}
}
$msg = $outq['outq_notify'];
if ($outq['outq_driver'] === 'nomad') {
$result = Libzot::nomad($outq['outq_posturl'],$msg,$channel,$host_crypto);
}
else {
$result = Libzot::zot($outq['outq_posturl'],$msg,$channel,$host_crypto);
}
if ($result['success']) {
logger('deliver: remote nomad/zot delivery succeeded to ' . $outq['outq_posturl']);
Libzot::process_response($outq['outq_posturl'], $result, $outq);
} else {
$dr = q(
"select * from dreport where dreport_queue = '%s'",
dbesc($outq['outq_hash'])
);
// update every queue entry going to this site with the most recent communication error
q(
"update dreport set dreport_log = '%s' where dreport_site = '%s'",
dbesc(z_curl_error($result)),
dbesc($dr[0]['dreport_site'])
);
logger('deliver: remote nomad/zot delivery failed to ' . $outq['outq_posturl']);
logger('deliver: remote nomad/zot delivery fail data: ' . print_r($result, true), LOGGER_DATA);
self::update($outq['outq_hash'], 10);
}
}
return;
}
}

33
Code/Lib/SConfig.php Normal file
View file

@ -0,0 +1,33 @@
<?php
namespace Code\Lib;
/**
* @brief Site configuration storage is built on top of the under-utilised xconfig.
*
* @see XConfig
*/
class SConfig
{
public static function Load($server_id)
{
return XConfig::Load('s_' . $server_id);
}
public static function Get($server_id, $family, $key, $default = false)
{
return XConfig::Get('s_' . $server_id, $family, $key, $default);
}
public static function Set($server_id, $family, $key, $value)
{
return XConfig::Set('s_' . $server_id, $family, $key, $value);
}
public static function Delete($server_id, $family, $key)
{
return XConfig::Delete('s_' . $server_id, $family, $key);
}
}

298
Code/Lib/ServiceClass.php Normal file
View file

@ -0,0 +1,298 @@
<?php
namespace Code\Lib;
use App;
use Code\Lib\Channel;
use Code\Extend\Hook;
class ServiceClass {
/**
* @brief Called when creating a new channel.
*
* Checks the account's service class and number of current channels to determine
* whether creating a new channel is within the current service class constraints.
*
* @param int $account_id
* Account_id used for this request
*
* @returns associative array with:
* * \e boolean \b success boolean true if creating a new channel is allowed for this account
* * \e string \b message (optional) if success is false, optional error text
* * \e int \b total_identities
*/
public static function identity_check_service_class($account_id)
{
$ret = array('success' => false, 'message' => '');
$r = q(
"select count(channel_id) as total from channel where channel_account_id = %d and channel_removed = 0 ",
intval($account_id)
);
if (! ($r && count($r))) {
$ret['total_identities'] = 0;
$ret['message'] = t('Unable to obtain identity information from database');
return $ret;
}
$ret['total_identities'] = intval($r[0]['total']);
if (! self::account_allows($account_id, 'total_identities', $r[0]['total'])) {
$ret['message'] .= self::upgrade_message();
return $ret;
}
$ret['success'] = true;
return $ret;
}
/**
* @brief Checks for accounts that have past their expiration date.
*
* If the account has a service class which is not the site default,
* the service class is reset to the site default and expiration reset to never.
* If the account has no service class it is expired and subsequently disabled.
* called from include/poller.php as a scheduled task.
*
* Reclaiming resources which are no longer within the service class limits is
* not the job of this function, but this can be implemented by plugin if desired.
* Default behaviour is to stop allowing additional resources to be consumed.
*/
public static function downgrade_accounts()
{
$r = q(
"select * from account where not ( account_flags & %d ) > 0
and account_expires > '%s'
and account_expires < %s ",
intval(ACCOUNT_EXPIRED),
dbesc(NULL_DATE),
db_getfunc('UTC_TIMESTAMP')
);
if (! $r) {
return;
}
$basic = get_config('system', 'default_service_class');
foreach ($r as $rr) {
if (($basic) && ($rr['account_service_class']) && ($rr['account_service_class'] != $basic)) {
$x = q(
"UPDATE account set account_service_class = '%s', account_expires = '%s'
where account_id = %d",
dbesc($basic),
dbesc(NULL_DATE),
intval($rr['account_id'])
);
$ret = [ 'account' => $rr ];
Hook::call('account_downgrade', $ret);
logger('downgrade_accounts: Account id ' . $rr['account_id'] . ' downgraded.');
} else {
$x = q(
"UPDATE account SET account_flags = (account_flags | %d) where account_id = %d",
intval(ACCOUNT_EXPIRED),
intval($rr['account_id'])
);
$ret = [ 'account' => $rr ];
Hook::call('account_downgrade', $ret);
logger('downgrade_accounts: Account id ' . $rr['account_id'] . ' expired.');
}
}
}
/**
* @brief Check service_class restrictions.
*
* If there are no service_classes defined, everything is allowed.
* If $usage is supplied, we check against a maximum count and return true if
* the current usage is less than the subscriber plan allows. Otherwise we
* return boolean true or false if the property is allowed (or not) in this
* subscriber plan. An unset property for this service plan means the property
* is allowed, so it is only necessary to provide negative properties for each
* plan, or what the subscriber is not allowed to do.
*
* Like account_service_class_allows() but queries directly by account rather
* than channel. Service classes are set for accounts, so we look up the
* account for the channel and fetch the service class restrictions of the
* account.
*
* @see account_service_class_allows() if you have a channel_id already
* @see service_class_fetch()
*
* @param int $uid The channel_id to check
* @param string $property The service class property to check for
* @param string|bool $usage (optional) The value to check against
* @return bool
*/
public static function allows($uid, $property, $usage = false)
{
$limit = self::fetch($uid, $property);
if ($limit === false) {
return true; // No service class set => everything is allowed
}
$limit = engr_units_to_bytes($limit);
if ($usage === false) {
// We use negative values for not allowed properties in a subscriber plan
return (($limit) ? (bool) $limit : true);
} else {
return (((intval($usage)) < intval($limit)) ? true : false);
}
}
/**
* @brief Check service class restrictions by account.
*
* If there are no service_classes defined, everything is allowed.
* If $usage is supplied, we check against a maximum count and return true if
* the current usage is less than the subscriber plan allows. Otherwise we
* return boolean true or false if the property is allowed (or not) in this
* subscriber plan. An unset property for this service plan means the property
* is allowed, so it is only necessary to provide negative properties for each
* plan, or what the subscriber is not allowed to do.
*
* Like service_class_allows() but queries directly by account rather than channel.
*
* @see service_class_allows() if you have a channel_id instead of an account_id
* @see account_service_class_fetch()
*
* @param int $aid The account_id to check
* @param string $property The service class property to check for
* @param int|bool $usage (optional) The value to check against
* @return bool
*/
public static function account_allows($aid, $property, $usage = false)
{
$limit = self::account_fetch($aid, $property);
if ($limit === false) {
return true; // No service class is set => everything is allowed
}
$limit = engr_units_to_bytes($limit);
if ($usage === false) {
// We use negative values for not allowed properties in a subscriber plan
return (($limit) ? (bool) $limit : true);
} else {
return (((intval($usage)) < intval($limit)) ? true : false);
}
}
/**
* @brief Queries a service class value for a channel and property.
*
* Service classes are set for accounts, so look up the account for this channel
* and fetch the service classe of the account.
*
* If no service class is available it returns false and everything should be
* allowed.
*
* @see account_service_class_fetch()
*
* @param int $uid The channel_id to query
* @param string $property The service property name to check for
* @return bool|int
*
* @todo Should we merge this with account_service_class_fetch()?
*/
public static function fetch($uid, $property)
{
if ($uid == local_channel()) {
$service_class = App::$account['account_service_class'];
} else {
$r = q(
"select account_service_class
from channel c, account a
where c.channel_account_id = a.account_id and c.channel_id = %d limit 1",
intval($uid)
);
if ($r) {
$service_class = $r[0]['account_service_class'];
}
}
if (! $service_class) {
return false; // everything is allowed
}
$arr = get_config('service_class', $service_class);
if (! is_array($arr) || (! count($arr))) {
return false;
}
return((array_key_exists($property, $arr)) ? $arr[$property] : false);
}
/**
* @brief Queries a service class value for an account and property.
*
* Like service_class_fetch() but queries by account rather than channel.
*
* @see service_class_fetch() if you have channel_id.
* @see account_service_class_allows()
*
* @param int $aid The account_id to query
* @param string $property The service property name to check for
* @return bool|int
*/
public static function account_fetch($aid, $property)
{
$r = q(
"select account_service_class as service_class from account where account_id = %d limit 1",
intval($aid)
);
if ($r !== false && count($r)) {
$service_class = $r[0]['service_class'];
}
if (! x($service_class)) {
return false; // everything is allowed
}
$arr = get_config('service_class', $service_class);
if (! is_array($arr) || (! count($arr))) {
return false;
}
return((array_key_exists($property, $arr)) ? $arr[$property] : false);
}
public static function upgrade_link($bbcode = false)
{
$l = get_config('service_class', 'upgrade_link');
if (! $l) {
return '';
}
if ($bbcode) {
$t = sprintf('[zrl=%s]' . t('Click here to upgrade.') . '[/zrl]', $l);
} else {
$t = sprintf('<a href="%s">' . t('Click here to upgrade.') . '</div>', $l);
}
return $t;
}
public static function upgrade_message($bbcode = false)
{
$x = self::upgrade_link($bbcode);
return t('This action exceeds the limits set by your subscription plan.') . (($x) ? ' ' . $x : '') ;
}
public static function upgrade_bool_message($bbcode = false)
{
$x = self::upgrade_link($bbcode);
return t('This action is not available under your subscription plan.') . (($x) ? ' ' . $x : '') ;
}
}

249
Code/Lib/Share.php Normal file
View file

@ -0,0 +1,249 @@
<?php
namespace Code\Lib;
use App;
use Code\Daemon\Run;
use Code\Lib\Libsync;
use Code\Lib\Channel;
use Code\Extend\Hook;
class Share
{
private $item = null;
private $attach = null;
public function __construct($post_id)
{
if (! $post_id) {
return;
}
if (is_array($post_id)) {
$this->item = $post_id;
return;
}
if (! (local_channel() || remote_channel())) {
return;
}
$r = q(
"SELECT * from item left join xchan on author_xchan = xchan_hash WHERE id = %d LIMIT 1",
intval($post_id)
);
if (! $r) {
return;
}
if (($r[0]['item_private']) && ($r[0]['xchan_network'] !== 'rss')) {
return;
}
$sql_extra = item_permissions_sql($r[0]['uid']);
$r = q(
"select * from item where id = %d $sql_extra",
intval($post_id)
);
if (! $r) {
return;
}
if (! in_array($r[0]['mimetype'], [ 'text/bbcode', 'text/x-multicode' ])) {
return;
}
/** @FIXME eventually we want to post remotely via rpost on your home site */
// When that works remove this next bit:
if (! local_channel()) {
return;
}
xchan_query($r);
$this->item = array_shift($r);
$arr = [];
$owner_uid = $this->item['uid'];
$owner_aid = $this->item['aid'];
$channel = Channel::from_id($this->item['uid']);
$observer = App::get_observer();
$can_comment = false;
if ((array_key_exists('owner', $this->item)) && intval($this->item['owner']['abook_self'])) {
$can_comment = perm_is_allowed($this->item['uid'], $observer['xchan_hash'], 'post_comments');
} else {
$can_comment = can_comment_on_post($observer['xchan_hash'], $this->item);
}
if (! $can_comment) {
return;
}
$r = q(
"select * from xchan where xchan_hash = '%s' limit 1",
dbesc($this->item['owner_xchan'])
);
if ($r) {
$thread_owner = array_shift($r);
} else {
return;
}
$r = q(
"select * from xchan where xchan_hash = '%s' limit 1",
dbesc($this->item['author_xchan'])
);
if ($r) {
$item_author = array_shift($r);
} else {
return;
}
if (! $this->attach) {
$this->attach = [];
}
$this->attach[] = [
'href' => $this->item['mid'],
'type' => 'application/activity+json',
'title' => $this->item['mid']
];
if ($item_author['network'] === 'activitypub') {
// for Mastodon compatibility, send back an ActivityPub Announce activity.
// We don't need or want these on our own network as there is no mechanism for providing
// a fair-use defense to copyright claims and frivolous lawsuits.
$arr['aid'] = $owner_aid;
$arr['uid'] = $owner_uid;
$arr['item_origin'] = 1;
$arr['item_wall'] = $this->item['item_wall'];
$arr['uuid'] = new_uuid();
$arr['mid'] = z_root() . '/item/' . $arr['uuid'];
$arr['mid'] = str_replace('/item/', '/activity/', $arr['mid']);
$arr['parent_mid'] = $this->item['mid'];
$mention = '@[zrl=' . $this->item['author']['xchan_url'] . ']' . $this->item['author']['xchan_name'] . '[/zrl]';
$arr['body'] = sprintf(t('&#x1f501; Repeated %1$s\'s %2$s'), $mention, $this->item['obj_type']);
$arr['author_xchan'] = $observer['xchan_hash'];
$arr['owner_xchan'] = $this->item['author_xchan'];
$arr['obj'] = $this->item['obj'];
$arr['obj_type'] = $this->item['obj_type'];
$arr['verb'] = 'Announce';
$post = item_store($arr);
$post_id = $post['item_id'];
$arr['id'] = $post_id;
Hook::call('post_local_end', $arr);
$r = q(
"select * from item where id = %d",
intval($post_id)
);
if ($r) {
xchan_query($r);
$sync_item = fetch_post_tags($r);
Libsync::build_sync_packet($channel['channel_id'], [ 'item' => [ encode_item($sync_item[0], true) ] ]);
}
Run::Summon([ 'Notifier','like',$post_id ]);
}
return;
}
public function obj()
{
$obj = [];
if (! $this->item) {
return $obj;
}
$obj['type'] = $this->item['obj_type'];
$obj['id'] = $this->item['mid'];
$obj['content'] = bbcode($this->item['body']);
$obj['source'] = [
'mediaType' => $this->item['mimetype'],
'content' => $this->item['body']
];
$obj['name'] = $this->item['title'];
$obj['published'] = $this->item['created'];
$obj['updated'] = $this->item['edited'];
$obj['attributedTo'] = ((strpos($this->item['author']['xchan_hash'], 'http') === 0)
? $this->item['author']['xchan_hash']
: $this->item['author']['xchan_url']);
return $obj;
}
public function get_attach()
{
return $this->attach;
}
public function bbcode()
{
$bb = EMPTY_STR;
if (! $this->item) {
return $bb;
}
if (! $this->item['author']) {
$author = q(
"select * from xchan where xchan_hash = '%s' limit 1",
dbesc($this->item['author_xchan'])
);
if ($author) {
$this->item['author'] = array_shift($author);
}
}
$special_object = (in_array($this->item['obj_type'], [ ACTIVITY_OBJ_PHOTO, 'Event', 'Question' ]) ? true : false);
if ($special_object) {
$object = json_decode($this->item['obj'], true);
$special = (($object['source']) ? $object['source']['content'] : $object['body']);
}
if (strpos($this->item['body'], "[/share]") !== false) {
$pos = strpos($this->item['body'], "[share");
$bb = substr($this->item['body'], $pos);
} else {
$bb = "[share author='" . urlencode($this->item['author']['xchan_name']) .
"' profile='" . $this->item['author']['xchan_url'] .
"' portable_id='" . $this->item['author']['xchan_hash'] .
"' avatar='" . $this->item['author']['xchan_photo_s'] .
"' link='" . $this->item['plink'] .
"' auth='" . (in_array($this->item['author']['network'],['nomad','zot6']) ? 'true' : 'false') .
"' posted='" . $this->item['created'] .
"' message_id='" . $this->item['mid'] .
"']";
if ($this->item['title']) {
$bb .= '[b]' . $this->item['title'] . '[/b]' . "\r\n";
}
if ($this->item['summary']) {
$bb .= $this->item['summary'] . "\r\n";
}
$bb .= (($special_object) ? $special . "\r\n" . $this->item['body'] : $this->item['body']);
$bb .= "[/share]";
}
return $bb;
}
}

696
Code/Lib/Socgraph.php Normal file
View file

@ -0,0 +1,696 @@
<?php
namespace Code\Lib;
use Code\Lib\Libzot;
use Code\Lib\Libzotdir;
use Code\Lib\Zotfinger;
use Code\Lib\ASCollection;
use Code\Render\Theme;
class Socgraph {
/**
* poco_load
*
* xchan is your connection
* We will load their friend list, and store in xlink_xchan your connection hash and xlink_link the hash for each connection
* If xchan isn't provided we will load the list of people from url who have indicated they are willing to be friends with
* new folks and add them to xlink with no xlink_xchan.
*
* Old behaviour: (documentation only):
* Given a contact-id (minimum), load the PortableContacts friend list for that contact,
* and add the entries to the gcontact (Global Contact) table, or update existing entries
* if anything (name or photo) has changed.
* We use normalised urls for comparison which ignore http vs https and www.domain vs domain
*
* Once the global contact is stored add (if necessary) the contact linkage which associates
* the given uid, cid to the global contact entry. There can be many uid/cid combinations
* pointing to the same global contact id.
*
* @param string $xchan
* @param string $url
*/
public static function poco_load($xchan = '', $url = null)
{
if (self::ap_poco_load($xchan)) {
return;
}
if ($xchan && ! $url) {
$r = q(
"select xchan_connurl from xchan where xchan_hash = '%s' limit 1",
dbesc($xchan)
);
if ($r) {
$url = $r[0]['xchan_connurl'];
}
}
if (! $url) {
logger('poco_load: no url');
return;
}
$max = intval(get_config('system', 'max_imported_follow', MAX_IMPORTED_FOLLOW));
if (! intval($max)) {
return;
}
$url = $url . '?f=&fields=displayName,hash,urls,photos' ;
logger('poco_load: ' . $url, LOGGER_DEBUG);
$s = z_fetch_url($url);
if (! $s['success']) {
if ($s['return_code'] == 401) {
logger('poco_load: protected');
} elseif ($s['return_code'] == 404) {
logger('poco_load: nothing found');
} else {
logger('poco_load: returns ' . print_r($s, true), LOGGER_DATA);
}
return;
}
$j = json_decode($s['body'], true);
if (! $j) {
logger('poco_load: unable to json_decode returned data.');
return;
}
logger('poco_load: ' . print_r($j, true), LOGGER_DATA);
if ($xchan) {
if (array_key_exists('chatrooms', $j) && is_array($j['chatrooms'])) {
foreach ($j['chatrooms'] as $room) {
if ((! $room['url']) || (! $room['desc'])) {
continue;
}
$r = q(
"select * from xchat where xchat_url = '%s' and xchat_xchan = '%s' limit 1",
dbesc($room['url']),
dbesc($xchan)
);
if ($r) {
q(
"update xchat set xchat_edited = '%s' where xchat_id = %d",
dbesc(datetime_convert()),
intval($r[0]['xchat_id'])
);
} else {
$x = q(
"insert into xchat ( xchat_url, xchat_desc, xchat_xchan, xchat_edited )
values ( '%s', '%s', '%s', '%s' ) ",
dbesc(escape_tags($room['url'])),
dbesc(escape_tags($room['desc'])),
dbesc($xchan),
dbesc(datetime_convert())
);
}
}
}
q(
"delete from xchat where xchat_edited < %s - INTERVAL %s and xchat_xchan = '%s' ",
db_utcnow(),
db_quoteinterval('7 DAY'),
dbesc($xchan)
);
}
if (! ((x($j, 'entry')) && (is_array($j['entry'])))) {
logger('poco_load: no entries');
return;
}
$total = 0;
foreach ($j['entry'] as $entry) {
$profile_url = '';
$profile_photo = '';
$address = '';
$name = '';
$hash = '';
$rating = 0;
$network = '';
$name = $entry['displayName'];
$hash = $entry['hash'];
if (x($entry, 'urls') && is_array($entry['urls'])) {
foreach ($entry['urls'] as $url) {
if ($url['type'] == 'profile') {
$profile_url = $url['value'];
continue;
}
if (in_array($url['type'], ['nomad', 'zot6', 'activitypub'])) {
$network = $url['type'];
$address = str_replace('acct:', '', $url['value']);
continue;
}
}
}
if (x($entry, 'photos') && is_array($entry['photos'])) {
foreach ($entry['photos'] as $photo) {
if ($photo['type'] == 'profile') {
$profile_photo = $photo['value'];
continue;
}
}
}
if (! in_array($network, ['nomad', 'zot6', 'activitypub'])) {
continue;
}
if ((! $name) || (! $profile_url) || (! $profile_photo) || (! $hash) || (! $address)) {
logger('poco_load: missing data');
logger('poco_load: ' . print_r($entry, true), LOGGER_DATA);
continue;
}
$x = q(
"select xchan_hash from xchan where ( xchan_hash = '%s' or xchan_url = '%s' ) order by xchan_network desc limit 1",
dbesc($hash),
dbesc($hash)
);
// We've never seen this person before. Import them.
if (($x !== false) && (! count($x))) {
if ($address) {
if (in_array($network, ['nomad', 'zot6', 'activitypub'])) {
$wf = discover_by_webbie($profile_url);
if ($wf) {
$x = q(
"select xchan_hash from xchan where ( xchan_hash = '%s' or xchan_url = '%s') order by xchan_network desc limit 1",
dbesc($wf),
dbesc($wf)
);
if ($x) {
$hash = $x[0]['xchan_hash'];
}
}
if (! $x) {
continue;
}
}
} else {
continue;
}
}
$total++;
$r = q(
"select * from xlink where xlink_xchan = '%s' and xlink_link = '%s' and xlink_static = 0 limit 1",
dbesc($xchan),
dbesc($hash)
);
if (! $r) {
q(
"insert into xlink ( xlink_xchan, xlink_link, xlink_rating, xlink_rating_text, xlink_sig, xlink_updated, xlink_static ) values ( '%s', '%s', %d, '%s', '%s', '%s', 0 ) ",
dbesc($xchan),
dbesc($hash),
intval(0),
dbesc(''),
dbesc(''),
dbesc(datetime_convert())
);
} else {
q(
"update xlink set xlink_updated = '%s' where xlink_id = %d",
dbesc(datetime_convert()),
intval($r[0]['xlink_id'])
);
}
$total++;
if ($total > $max) {
break;
}
}
logger("poco_load: loaded $total entries", LOGGER_DEBUG);
q(
"delete from xlink where xlink_xchan = '%s' and xlink_updated < %s - INTERVAL %s and xlink_static = 0",
dbesc($xchan),
db_utcnow(),
db_quoteinterval('7 DAY')
);
}
public static function ap_poco_load($xchan)
{
$max = intval(get_config('system', 'max_imported_follow', MAX_IMPORTED_FOLLOW));
if (! intval($max)) {
return;
}
if ($xchan) {
$cl = get_xconfig($xchan, 'activitypub', 'collections');
if (is_array($cl) && $cl) {
$url = ((array_key_exists('following', $cl)) ? $cl['following'] : '');
} else {
return false;
}
}
if (! $url) {
logger('ap_poco_load: no url');
return;
}
$obj = new ASCollection($url, '', 0, $max);
$friends = $obj->get();
if (! $friends) {
return;
}
foreach ($friends as $entry) {
$hash = EMPTY_STR;
$x = q(
"select xchan_hash from xchan where (xchan_hash = '%s' or xchan_url = '%s') order by xchan_network desc limit 1",
dbesc($entry),
dbesc($entry)
);
if ($x) {
$hash = $x[0]['xchan_hash'];
} else {
// We've never seen this person before. Import them.
$wf = discover_by_webbie($entry);
if ($wf) {
$x = q(
"select xchan_hash from xchan where (xchan_hash = '%s' or xchan_url = '%s') order by xchan_network desc limit 1",
dbesc($wf),
dbesc($wf)
);
if ($x) {
$hash = $x[0]['xchan_hash'];
}
}
}
if (! $hash) {
continue;
}
$total++;
$r = q(
"select * from xlink where xlink_xchan = '%s' and xlink_link = '%s' and xlink_static = 0 limit 1",
dbesc($xchan),
dbesc($hash)
);
if (! $r) {
q(
"insert into xlink ( xlink_xchan, xlink_link, xlink_rating, xlink_rating_text, xlink_sig, xlink_updated, xlink_static ) values ( '%s', '%s', %d, '%s', '%s', '%s', 0 ) ",
dbesc($xchan),
dbesc($hash),
intval(0),
dbesc(''),
dbesc(''),
dbesc(datetime_convert())
);
} else {
q(
"update xlink set xlink_updated = '%s' where xlink_id = %d",
dbesc(datetime_convert()),
intval($r[0]['xlink_id'])
);
}
}
logger("ap_poco_load: loaded $total entries", LOGGER_DEBUG);
q(
"delete from xlink where xlink_xchan = '%s' and xlink_updated < %s - INTERVAL %s and xlink_static = 0",
dbesc($xchan),
db_utcnow(),
db_quoteinterval('7 DAY')
);
return true;
}
public static function count_common_friends($uid, $xchan)
{
$r = q(
"SELECT count(xlink_id) as total from xlink where xlink_xchan = '%s' and xlink_static = 0 and xlink_link in
(select abook_xchan from abook where abook_xchan != '%s' and abook_channel = %d and abook_self = 0 )",
dbesc($xchan),
dbesc($xchan),
intval($uid)
);
if ($r) {
return $r[0]['total'];
}
return 0;
}
public static function common_friends($uid, $xchan, $start = 0, $limit = 100000000, $shuffle = false)
{
$rand = db_getfunc('rand');
if ($shuffle) {
$sql_extra = " order by $rand ";
} else {
$sql_extra = " order by xchan_name asc ";
}
$r = q(
"SELECT * from xchan left join xlink on xlink_link = xchan_hash where xlink_xchan = '%s' and xlink_static = 0 and xlink_link in
(select abook_xchan from abook where abook_xchan != '%s' and abook_channel = %d and abook_self = 0 ) $sql_extra limit %d offset %d",
dbesc($xchan),
dbesc($xchan),
intval($uid),
intval($limit),
intval($start)
);
return $r;
}
public static function suggestion_query($uid, $myxchan, $start = 0, $limit = 120)
{
if ((! $uid) || (! $myxchan)) {
return [];
}
$r1 = q(
"SELECT count(xlink_xchan) as total, xchan.* from xchan
left join xlink on xlink_link = xchan_hash
where xlink_xchan in ( select abook_xchan from abook where abook_channel = %d )
and not xlink_link in ( select abook_xchan from abook where abook_channel = %d )
and not xlink_link in ( select xchan from xign where uid = %d )
and xlink_xchan != ''
and xchan_hidden = 0
and xchan_deleted = 0
and xlink_static = 0
group by xchan_hash order by total desc limit %d offset %d ",
intval($uid),
intval($uid),
intval($uid),
intval($limit),
intval($start)
);
if (! $r1) {
$r1 = [];
}
$r2 = q(
"SELECT count(xtag_hash) as total, xchan.* from xchan
left join xtag on xtag_hash = xchan_hash
where xtag_hash != '%s'
and not xtag_hash in ( select abook_xchan from abook where abook_channel = %d )
and xtag_term in ( select xtag_term from xtag where xtag_hash = '%s' )
and not xtag_hash in ( select xchan from xign where uid = %d )
and xchan_hidden = 0
and xchan_deleted = 0
group by xchan_hash order by total desc limit %d offset %d ",
dbesc($myxchan),
intval($uid),
dbesc($myxchan),
intval($uid),
intval($limit),
intval($start)
);
if (! $r2) {
$r2 = [];
}
foreach ($r2 as $r) {
$found = false;
for ($x = 0; $x < count($r1); $x++) {
if ($r['xchan_hash'] === $r1[$x]['xchan_hash']) {
$r1[$x]['total'] = intval($r1[$x]['total']) + intval($r['total']);
$found = true;
continue;
}
}
if (! $found) {
$r1[] = $r;
}
}
usort($r1, 'self::socgraph_total_sort');
return ($r1);
}
public static function socgraph_total_sort($a, $b)
{
if ($a['total'] === $b['total']) {
return 0;
}
return((intval($a['total']) < intval($b['total'])) ? 1 : -1 );
}
public static function poco()
{
$system_mode = false;
if (observer_prohibited()) {
logger('mod_poco: block_public');
http_status_exit(401);
}
$observer = App::get_observer();
if (argc() > 1) {
$user = notags(trim(argv(1)));
}
if (! (isset($user) && $user)) {
$c = q("select * from pconfig where cat = 'system' and k = 'suggestme' and v = '1'");
if (! $c) {
logger('mod_poco: system mode. No candidates.', LOGGER_DEBUG);
http_status_exit(404);
}
$system_mode = true;
}
$format = ((isset($_REQUEST['format']) && $_REQUEST['format']) ? $_REQUEST['format'] : 'json');
$justme = false;
if (argc() > 2 && argv(2) === '@me') {
$justme = true;
}
if (argc() > 3) {
if (argv(3) === '@all') {
$justme = false;
} elseif (argv(3) === '@self') {
$justme = true;
}
}
if (argc() > 4 && intval(argv(4)) && $justme == false) {
$cid = intval(argv(4));
}
if (! $system_mode) {
$r = q(
"SELECT channel_id from channel where channel_address = '%s' limit 1",
dbesc($user)
);
if (! $r) {
logger('mod_poco: user mode. Account not found. ' . $user);
http_status_exit(404);
}
$channel_id = $r[0]['channel_id'];
$ohash = (($observer) ? $observer['xchan_hash'] : '');
if (! perm_is_allowed($channel_id, $ohash, 'view_contacts')) {
logger('mod_poco: user mode. Permission denied for ' . $ohash . ' user: ' . $user);
http_status_exit(401);
}
}
if (isset($justme) && $justme) {
$sql_extra = " and abook_self = 1 ";
} else {
$sql_extra = " and abook_self = 0 ";
}
if (isset($cid) && $cid) {
$sql_extra = sprintf(" and abook_id = %d and abook_archived = 0 and abook_hidden = 0 and abook_pending = 0 ", intval($cid));
}
if (isset($system_mode) && $system_mode) {
$r = q("SELECT count(*) as total from abook where abook_self = 1
and abook_channel in (select uid from pconfig where cat = 'system' and k = 'suggestme' and v = '1') ");
} else {
$r = q(
"SELECT count(*) as total from abook where abook_channel = %d
$sql_extra ",
intval($channel_id)
);
$rooms = q(
"select * from menu_item where ( mitem_flags & " . intval(MENU_ITEM_CHATROOM) . " ) > 0 and allow_cid = '' and allow_gid = '' and deny_cid = '' and deny_gid = '' and mitem_channel_id = %d",
intval($channel_id)
);
}
if ($r) {
$totalResults = intval($r[0]['total']);
} else {
$totalResults = 0;
}
$startIndex = ((isset($_GET['startIndex'])) ? intval($_GET['startIndex']) : 0);
if ($startIndex < 0) {
$startIndex = 0;
}
$itemsPerPage = ((isset($_GET['count']) && intval($_GET['count'])) ? intval($_GET['count']) : $totalResults);
if ($system_mode) {
$r = q(
"SELECT abook.*, xchan.* from abook left join xchan on abook_xchan = xchan_hash where abook_self = 1
and abook_channel in (select uid from pconfig where cat = 'system' and k = 'suggestme' and v = '1')
limit %d offset %d ",
intval($itemsPerPage),
intval($startIndex)
);
} else {
$r = q(
"SELECT abook.*, xchan.* from abook left join xchan on abook_xchan = xchan_hash where abook_channel = %d
$sql_extra LIMIT %d OFFSET %d",
intval($channel_id),
intval($itemsPerPage),
intval($startIndex)
);
}
$ret = [];
if (x($_GET, 'sorted')) {
$ret['sorted'] = 'false';
}
if (x($_GET, 'filtered')) {
$ret['filtered'] = 'false';
}
if (x($_GET, 'updatedSince')) {
$ret['updateSince'] = 'false';
}
$ret['startIndex'] = (string) $startIndex;
$ret['itemsPerPage'] = (string) $itemsPerPage;
$ret['totalResults'] = (string) $totalResults;
if ($rooms) {
$ret['chatrooms'] = [];
foreach ($rooms as $room) {
$ret['chatrooms'][] = array('url' => $room['mitem_link'], 'desc' => $room['mitem_desc']);
}
}
$ret['entry'] = [];
$fields_ret = array(
'id' => false,
'guid' => false,
'guid_sig' => false,
'hash' => false,
'displayName' => false,
'urls' => false,
'preferredUsername' => false,
'photos' => false,
'rating' => false
);
if ((! x($_GET, 'fields')) || ($_GET['fields'] === '@all')) {
foreach ($fields_ret as $k => $v) {
$fields_ret[$k] = true;
}
} else {
$fields_req = explode(',', $_GET['fields']);
foreach ($fields_req as $f) {
$fields_ret[trim($f)] = true;
}
}
if (is_array($r)) {
if (count($r)) {
foreach ($r as $rr) {
$entry = [];
if ($fields_ret['id']) {
$entry['id'] = $rr['abook_id'];
}
if ($fields_ret['guid']) {
$entry['guid'] = $rr['xchan_guid'];
}
if ($fields_ret['guid_sig']) {
$entry['guid_sig'] = $rr['xchan_guid_sig'];
}
if ($fields_ret['hash']) {
$entry['hash'] = $rr['xchan_hash'];
}
if ($fields_ret['displayName']) {
$entry['displayName'] = $rr['xchan_name'];
}
if ($fields_ret['urls']) {
$entry['urls'] = array(array('value' => $rr['xchan_url'], 'type' => 'profile'));
$network = $rr['xchan_network'];
if ($rr['xchan_addr']) {
$entry['urls'][] = array('value' => 'acct:' . $rr['xchan_addr'], 'type' => $network);
}
}
if ($fields_ret['preferredUsername']) {
$entry['preferredUsername'] = substr($rr['xchan_addr'], 0, strpos($rr['xchan_addr'], '@'));
}
if ($fields_ret['photos']) {
$entry['photos'] = array(array('value' => $rr['xchan_photo_l'], 'mimetype' => $rr['xchan_photo_mimetype'], 'type' => 'profile'));
}
$ret['entry'][] = $entry;
}
} else {
$ret['entry'][] = [];
}
} else {
http_status_exit(500);
}
if ($format === 'xml') {
header('Content-type: text/xml');
echo replace_macros(Theme::get_template('poco_xml.tpl'), array_xmlify(array('$response' => $ret)));
http_status_exit(500);
}
if ($format === 'json') {
header('Content-type: application/json');
echo json_encode($ret);
killme();
} else {
http_status_exit(500);
}
}
}

62
Code/Lib/Statistics.php Normal file
View file

@ -0,0 +1,62 @@
<?php
namespace Code\Lib;
use Code\Lib\Config;
class Statistics {
function get_channels_all()
{
$r = q(
"select count(channel_id) as channels_total from channel left join account on account_id = channel_account_id
where account_flags = 0 "
);
$total = ($r) ? intval($r[0]['channels_total']) : 0;
Config::Set('system', 'channels_total_stat', $total);
return $total;
}
function get_channels_6mo()
{
$r = q(
"select channel_id from channel left join account on account_id = channel_account_id
where account_flags = 0 and channel_active > %s - INTERVAL %s",
db_utcnow(),
db_quoteinterval('6 MONTH')
);
$total = ($r) ? count($r) : 0;
Config::Set('system', 'channels_active_halfyear_stat', $total);
return $total;
}
function get_channels_1mo()
{
$r = q(
"select channel_id from channel left join account on account_id = channel_account_id
where account_flags = 0 and channel_active > %s - INTERVAL %s",
db_utcnow(),
db_quoteinterval('1 MONTH')
);
$total = ($r) ? count($r) : 0;
Config::Set('system', 'channels_active_monthly_stat', $total);
return $total;
}
function get_posts()
{
$posts = q("SELECT COUNT(*) AS local_posts FROM item WHERE item_wall = 1 and id = parent");
$total = ($posts) ? intval($posts[0]['local_posts']) : 0;
Config::Set('system', 'local_posts_stat', $total);
return $total;
}
function get_comments()
{
$posts = q("SELECT COUNT(*) AS local_posts FROM item WHERE item_wall = 1 and id != parent");
$total = ($posts) ? intval($posts[0]['local_posts']) : 0;
Config::Set('system', 'local_comments_stat', $total);
return $total;
}
}

129
Code/Lib/Stringsjs.php Normal file
View file

@ -0,0 +1,129 @@
<?php
namespace Code\Lib;
use Code\Render\Theme;
class Stringsjs {
public static function strings()
{
return replace_macros(Theme::get_template('js_strings.tpl'), array(
'$icon' => '/images/' . PLATFORM_NAME . '-64.png',
'$delitem' => t('Delete this item?'),
'$comment' => t('Comment'),
'$showmore' => sprintf(t('%s show all'), '<i class=\'fa fa-chevron-down\'></i>'),
'$showfewer' => sprintf(t('%s show less'), '<i class=\'fa fa-chevron-up\'></i>'),
'$divgrowmore' => sprintf(t('%s expand'), '<i class=\'fa fa-chevron-down\'></i>'),
'$divgrowless' => sprintf(t('%s collapse'), '<i class=\'fa fa-chevron-up\'></i>'),
'$pwshort' => t("Password too short"),
'$pwnomatch' => t("Passwords do not match"),
'$everybody' => t('everybody'),
'$passphrase' => t('Secret Passphrase'),
'$passhint' => t('Passphrase hint'),
'$permschange' => t('Notice: Permissions have changed but have not yet been submitted.'),
'$closeAll' => t('close all'),
'$nothingnew' => t('Nothing new here'),
'$rating_desc' => t('Rate This Channel (this is public)'),
'$rating_val' => t('Rating'),
'$rating_text' => t('Describe (optional)'),
'$submit' => t('Submit'),
'$linkurl' => t('Please enter a link URL'),
'$leavethispage' => t('Unsaved changes. Are you sure you wish to leave this page?'),
'$location' => t('Location'),
'$lovely' => t('lovely'),
'$wonderful' => t('wonderful'),
'$fantastic' => t('fantastic'),
'$great' => t('great'),
'$nick_invld1' => t('Your chosen nickname was either already taken or not valid. Please use our suggestion ('),
'$nick_invld2' => t(') or enter a new one.'),
'$nick_valid' => t('Thank you, this nickname is valid.'),
'$name_empty' => t('A channel name is required.'),
'$name_ok1' => t('This is a '),
'$name_ok2' => t(' channel name'),
'$pinned' => t('Pinned'),
'$pin_item' => t('Pin this post'),
'$unpin_item' => t('Unpin this post'),
'$tos' => t('Please accept terms to continue'),
// translatable prefix and suffix strings for jquery.timeago -
// using the defaults set below if left untranslated, empty strings if
// translated to "NONE" and the corresponding language strings
// if translated to anything else
'$t01' => ((t('timeago.prefixAgo') == 'timeago.prefixAgo') ? '' : ((t('timeago.prefixAgo') == 'NONE') ? '' : t('timeago.prefixAgo'))),
'$t02' => ((t('timeago.prefixFromNow') == 'timeago.prefixFromNow') ? '' : ((t('timeago.prefixFromNow') == 'NONE') ? '' : t('timeago.prefixFromNow'))),
'$t03' => ((t('timeago.suffixAgo') == 'timeago.suffixAgo') ? 'ago' : ((t('timeago.suffixAgo') == 'NONE') ? '' : t('timeago.suffixAgo'))),
'$t04' => ((t('timeago.suffixFromNow') == 'timeago.suffixFromNow') ? 'from now' : ((t('timeago.suffixFromNow') == 'NONE') ? '' : t('timeago.suffixFromNow'))),
// translatable main strings for jquery.timeago
'$t05' => t('less than a minute'),
'$t06' => t('about a minute'),
'$t07' => t('%d minutes'),
'$t08' => t('about an hour'),
'$t09' => t('about %d hours'),
'$t10' => t('a day'),
'$t11' => t('%d days'),
'$t12' => t('about a month'),
'$t13' => t('%d months'),
'$t14' => t('about a year'),
'$t15' => t('%d years'),
'$t16' => t(' '), // wordSeparator
'$t17' => ((t('timeago.numbers') != 'timeago.numbers') ? t('timeago.numbers') : '[]'),
'$January' => t('January'),
'$February' => t('February'),
'$March' => t('March'),
'$April' => t('April'),
'$May' => t('May', 'long'),
'$June' => t('June'),
'$July' => t('July'),
'$August' => t('August'),
'$September' => t('September'),
'$October' => t('October'),
'$November' => t('November'),
'$December' => t('December'),
'$Jan' => t('Jan'),
'$Feb' => t('Feb'),
'$Mar' => t('Mar'),
'$Apr' => t('Apr'),
'$MayShort' => t('May', 'short'),
'$Jun' => t('Jun'),
'$Jul' => t('Jul'),
'$Aug' => t('Aug'),
'$Sep' => t('Sep'),
'$Oct' => t('Oct'),
'$Nov' => t('Nov'),
'$Dec' => t('Dec'),
'$Sunday' => t('Sunday'),
'$Monday' => t('Monday'),
'$Tuesday' => t('Tuesday'),
'$Wednesday' => t('Wednesday'),
'$Thursday' => t('Thursday'),
'$Friday' => t('Friday'),
'$Saturday' => t('Saturday'),
'$Sun' => t('Sun'),
'$Mon' => t('Mon'),
'$Tue' => t('Tue'),
'$Wed' => t('Wed'),
'$Thu' => t('Thu'),
'$Fri' => t('Fri'),
'$Sat' => t('Sat'),
'$today' => t('today', 'calendar'),
'$month' => t('month', 'calendar'),
'$week' => t('week', 'calendar'),
'$day' => t('day', 'calendar'),
'$allday' => t('All day', 'calendar'),
'$channel_social' => t('A social networking profile that is public by default and private if desired'),
'$channel_social_restricted' => t('A social networking profile where content is private to your [Friends] Access List by default but can be made public if desired'),
'$channel_forum' => t('A public group where members are allowed to upload media by default'),
'$channel_forum_restricted' => t('A private group with no upload permission'),
'$channel_forum_moderated' => t('A public group where posts are moderated by the owner. The [moderated] permission may be removed from any group member once trust is established'),
'$channel_collection' => t('A sub-channel of your main channel - often devoted to a specific language or topic. Replies are sent back to your main channel'),
'$channel_collection_restricted' => t('A private sub-channel of your main channel - often devoted to a specific language or topic. Replies are sent back to your main channel'),
));
}
}

160
Code/Lib/SvgSanitizer.php Normal file
View file

@ -0,0 +1,160 @@
<?php
namespace Code\Lib;
use DomDocument;
use Code\Lib\Config;
/**
* SVGSantiizer
*
* Allowlist-based PHP SVG sanitizer.
*
* @link https://github.com/alister-/SVG-Sanitizer}
* @author Alister Norris
* @copyright Copyright (c) 2013 Alister Norris
* @license http://opensource.org/licenses/mit-license.php The MIT License
* @package svgsanitizer
*/
class SvgSanitizer
{
private $xmlDoc; // PHP XML DOMDocument
private $removedattrs = [];
private static $allowed_functions = ['matrix', 'url', 'translate', 'rgb'];
// defines the allowlist of elements and attributes allowed.
private static $allowlist = [
'a' => ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'mask', 'opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'href', 'xlink:href', 'xlink:title'],
'circle' => ['class', 'clip-path', 'clip-rule', 'cx', 'cy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'mask', 'opacity', 'r', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],
'clipPath' => ['class', 'clipPathUnits', 'id'],
'defs' => [],
'style' => ['type'],
'desc' => [],
'ellipse' => ['class', 'clip-path', 'clip-rule', 'cx', 'cy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'mask', 'opacity', 'requiredFeatures', 'rx', 'ry', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],
'feGaussianBlur' => ['class', 'color-interpolation-filters', 'id', 'requiredFeatures', 'stdDeviation'],
'filter' => ['class', 'color-interpolation-filters', 'filterRes', 'filterUnits', 'height', 'id', 'primitiveUnits', 'requiredFeatures', 'width', 'x', 'xlink:href', 'y'],
'foreignObject' => ['class', 'font-size', 'height', 'id', 'opacity', 'requiredFeatures', 'style', 'transform', 'width', 'x', 'y'],
'g' => ['class', 'clip-path', 'clip-rule', 'id', 'display', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'font-family', 'font-size', 'font-style', 'font-weight', 'text-anchor'],
'image' => ['class', 'clip-path', 'clip-rule', 'filter', 'height', 'id', 'mask', 'opacity', 'requiredFeatures', 'style', 'systemLanguage', 'transform', 'width', 'x', 'xlink:href', 'xlink:title', 'y'],
'line' => ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'x1', 'x2', 'y1', 'y2'],
'linearGradient' => ['class', 'id', 'gradientTransform', 'gradientUnits', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'x1', 'x2', 'xlink:href', 'y1', 'y2'],
'marker' => ['id', 'class', 'markerHeight', 'markerUnits', 'markerWidth', 'orient', 'preserveAspectRatio', 'refX', 'refY', 'systemLanguage', 'viewBox'],
'mask' => ['class', 'height', 'id', 'maskContentUnits', 'maskUnits', 'width', 'x', 'y'],
'metadata' => ['class', 'id'],
'path' => ['class', 'clip-path', 'clip-rule', 'd', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],
'pattern' => ['class', 'height', 'id', 'patternContentUnits', 'patternTransform', 'patternUnits', 'requiredFeatures', 'style', 'systemLanguage', 'viewBox', 'width', 'x', 'xlink:href', 'y'],
'polygon' => ['class', 'clip-path', 'clip-rule', 'id', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'class', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'points', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],
'polyline' => ['class', 'clip-path', 'clip-rule', 'id', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'points', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],
'radialGradient' => ['class', 'cx', 'cy', 'fx', 'fy', 'gradientTransform', 'gradientUnits', 'id', 'r', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'xlink:href'],
'rect' => ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'id', 'mask', 'opacity', 'requiredFeatures', 'rx', 'ry', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'width', 'x', 'y'],
'stop' => ['class', 'id', 'offset', 'requiredFeatures', 'stop-color', 'stop-opacity', 'style', 'systemLanguage'],
'svg' => ['class', 'clip-path', 'clip-rule', 'filter', 'id', 'height', 'mask', 'preserveAspectRatio', 'requiredFeatures', 'style', 'systemLanguage', 'viewBox', 'width', 'x', 'xmlns', 'xmlns:se', 'xmlns:xlink', 'y'],
'switch' => ['class', 'id', 'requiredFeatures', 'systemLanguage'],
'symbol' => ['class', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'id', 'opacity', 'preserveAspectRatio', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'viewBox'],
'text' => ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'id', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'text-anchor', 'transform', 'x', 'xml:space', 'y'],
'textPath' => ['class', 'id', 'method', 'requiredFeatures', 'spacing', 'startOffset', 'style', 'systemLanguage', 'transform', 'xlink:href'],
'title' => [],
'tspan' => ['class', 'clip-path', 'clip-rule', 'dx', 'dy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'id', 'mask', 'opacity', 'requiredFeatures', 'rotate', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'text-anchor', 'textLength', 'transform', 'x', 'xml:space', 'y'],
'use' => ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'id', 'mask', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'transform', 'width', 'x', 'xlink:href', 'y'],
];
public function __construct()
{
$this->xmlDoc = new DOMDocument('1.0', 'UTF-8');
$this->xmlDoc->preserveWhiteSpace = false;
libxml_use_internal_errors(true);
}
// load XML SVG
public function load($file)
{
$this->xmlDoc->load($file);
}
public function loadXML($str)
{
if (!$str) {
logger('loadxml: empty input', LOGGER_DEBUG);
return false;
}
if (!$this->xmlDoc->loadXML($str)) {
logger('loadxml: ' . print_r(array_slice(libxml_get_errors(), 0, Config::Get('system', 'svg_backtrace_limit', 3)), true), LOGGER_DEBUG);
return false;
}
return true;
}
public function sanitize()
{
// all elements in xml doc
$allElements = $this->xmlDoc->getElementsByTagName('*');
// loop through all elements
for ($i = 0; $i < $allElements->length; $i++) {
$this->removedattrs = [];
$currentNode = $allElements->item($i);
// logger('current_node: ' . print_r($currentNode,true));
// array of allowed attributes in specific element
$allowlist_attr_arr = self::$allowlist[$currentNode->tagName];
// does element exist in allowlist?
if (isset($allowlist_attr_arr)) {
$total = $currentNode->attributes->length;
for ($x = 0; $x < $total; $x++) {
// get attributes name
$attrName = $currentNode->attributes->item($x)->nodeName;
// logger('checking: ' . print_r($currentNode->attributes->item($x),true));
$matches = false;
// check if attribute isn't in allowlist
if (!in_array($attrName, $allowlist_attr_arr)) {
$this->removedattrs[] = $attrName;
} // check for disallowed functions
elseif (
preg_match_all(
'/([a-zA-Z0-9]+)[\s]*\(/',
$currentNode->attributes->item($x)->textContent,
$matches,
PREG_SET_ORDER
)
) {
if ($attrName === 'text') {
continue;
}
foreach ($matches as $match) {
if (!in_array($match[1], self::$allowed_functions)) {
logger('queue_remove_function: ' . $match[1], LOGGER_DEBUG);
$this->removedattrs[] = $attrName;
}
}
}
}
if ($this->removedattrs) {
foreach ($this->removedattrs as $attr) {
$currentNode->removeAttribute($attr);
logger('removed: ' . $attr, LOGGER_DEBUG);
}
}
} // else remove element
else {
logger('remove_node: ' . print_r($currentNode, true));
$currentNode->parentNode->removeChild($currentNode);
}
}
return true;
}
public function saveSVG()
{
$this->xmlDoc->formatOutput = true;
return ($this->xmlDoc->saveXML());
}
}

170
Code/Lib/System.php Normal file
View file

@ -0,0 +1,170 @@
<?php
namespace Code\Lib;
use App;
use Code\Lib\Channel;
use Code\Extend\Hook;
use URLify;
class System
{
public static function get_platform_name()
{
if (is_array(App::$config) && is_array(App::$config['system']) && array_key_exists('platform_name', App::$config['system'])) {
return App::$config['system']['platform_name'];
}
return PLATFORM_NAME;
}
public static function get_site_name()
{
if (is_array(App::$config) && is_array(App::$config['system']) && App::$config['system']['sitename']) {
return App::$config['system']['sitename'];
}
return '';
}
public static function get_project_name()
{
$project = EMPTY_STR;
$name = self::get_site_name();
if ($name) {
$words = explode(' ', $name);
$project = strtolower(URLify::transliterate($words[0]));
}
if (!$project) {
$project = self::get_platform_name();
}
return $project;
}
public static function get_banner()
{
if (is_array(App::$config) && is_array(App::$config['system']) && array_key_exists('banner', App::$config['system']) && App::$config['system']['banner']) {
return App::$config['system']['banner'];
}
return self::get_site_name();
}
public static function get_project_icon()
{
$sys = Channel::get_system();
if ($sys) {
return z_root() . '/photo/profile/l/' . $sys['channel_id'];
}
if (is_array(App::$config) && is_array(App::$config['system']) && array_key_exists('icon', App::$config['system'])) {
return App::$config['system']['icon'];
}
return z_root() . '/images/' . PLATFORM_NAME . '-64.png';
}
public static function get_project_favicon()
{
if (is_array(App::$config) && is_array(App::$config['system']) && array_key_exists('favicon', App::$config['system'])) {
return App::$config['system']['favicon'];
}
return z_root() . '/images/' . PLATFORM_NAME . '.ico';
}
public static function get_project_version()
{
if (array_path_exists('system/hide_version', App::$config) && intval(App::$config['system']['hide_version'])) {
return '';
}
if (is_array(App::$config) && is_array(App::$config['system']) && array_key_exists('std_version', App::$config['system'])) {
return App::$config['system']['std_version'];
}
return self::get_std_version();
}
public static function get_update_version()
{
if (is_array(App::$config) && is_array(App::$config['system']) && App::$config['system']['hide_version']) {
return EMPTY_STR;
}
return DB_UPDATE_VERSION;
}
public static function get_notify_icon()
{
if (is_array(App::$config) && is_array(App::$config['system']) && App::$config['system']['email_notify_icon_url']) {
return App::$config['system']['email_notify_icon_url'];
}
return self::get_project_icon();
}
public static function get_site_icon()
{
if (is_array(App::$config) && is_array(App::$config['system']) && isset(App::$config['system']['site_icon_url']) && App::$config['system']['site_icon_url']) {
return App::$config['system']['site_icon_url'];
}
return self::get_project_icon();
}
public static function get_site_favicon()
{
if (is_array(App::$config) && is_array(App::$config['system']) && App::$config['system']['site_favicon_url']) {
return App::$config['system']['site_favicon_url'];
}
return self::get_project_favicon();
}
public static function get_project_link()
{
if (is_array(App::$config) && is_array(App::$config['system']) && App::$config['system']['project_link']) {
return App::$config['system']['project_link'];
}
return 'https://zotlabs.com/' . PLATFORM_NAME;
}
public static function get_project_srclink()
{
if (is_array(App::$config) && is_array(App::$config['system']) && App::$config['system']['project_srclink']) {
return App::$config['system']['project_srclink'];
}
if (PLATFORM_NAME === 'streams') {
return 'https://codeberg.org/streams/' . PLATFORM_NAME;
}
return 'https://codeberg.org/zot/' . PLATFORM_NAME;
}
public static function ebs()
{
if (defined('EBSSTATE')) {
return EBSSTATE;
}
return 'armed';
}
public static function get_zot_revision()
{
$x = [ 'revision' => ZOT_REVISION ];
Hook::call('zot_revision', $x);
return $x['revision'];
}
public static function get_std_version()
{
if (defined('STD_VERSION')) {
return STD_VERSION;
}
return '0.0.0';
}
public static function compatible_project($p)
{
if (in_array(strtolower($p), ['hubzilla', 'zap', 'red', 'misty', 'mistpark', 'redmatrix', 'osada', 'roadhouse','streams'])) {
return true;
}
return false;
}
}

1081
Code/Lib/ThreadItem.php Normal file

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more