diff --git a/.gitignore b/.gitignore
index b001b6b27..fa09c4ee6 100755
--- a/.gitignore
+++ b/.gitignore
@@ -37,7 +37,7 @@ pageheader.html
doc/SiteTOS.md
# themes except for redbasic
view/theme/*
-! view/theme/redbasic
+!view/theme/redbasic
# site theme schemas
view/theme/redbasic/schema/default.php
# Doxygen API documentation, run 'doxygen util/Doxyfile' to generate it
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 000000000..0b8e0430f
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,36 @@
+# Select image from https://hub.docker.com/_/php/
+image: php:7.1
+
+# Select what we should cache
+cache:
+ paths:
+ - vendor/
+
+variables:
+ # Configure mysql service (https://hub.docker.com/_/mysql/)
+ MYSQL_DATABASE: hello_world_test
+ MYSQL_ROOT_PASSWORD: mysql
+
+
+services:
+- mysql:5.7
+
+before_script:
+- apt-get update -yqq
+- apt-get install -yqq git mysql-server mysql-client libmcrypt-dev libpq-dev libcurl4-gnutls-dev libicu-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev
+# Install PHP extensions
+- docker-php-ext-install mbstring mcrypt pdo_mysql pdo_pgsql curl json intl gd xml zip bz2 opcache
+# Install & enable Xdebug for code coverage reports
+- pecl install xdebug
+- docker-php-ext-enable xdebug
+# Install and run Composer
+- curl -sS https://getcomposer.org/installer | php
+- php composer.phar install
+
+# We test PHP7 with MySQL, but we allow it to fail
+test:php:mysql:
+ script:
+ - echo "USE $MYSQL_DATABASE; $(cat ./install/schema_mysql.sql)" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE"
+ - echo "SHOW DATABASES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE"
+ - echo "USE $MYSQL_DATABASE; SHOW TABLES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE"
+ - vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-text
diff --git a/CHANGELOG b/CHANGELOG
index a1a68fd64..572a39fce 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,4 +1,104 @@
-Hubzilla 3.6 (????-??-??)
+Hubzilla 3.8 (2018-10-19)
+ - Re-implement basic build test via gitlab-ci
+ - Rework wiki encoding/decoding
+ - Implement improved worker (experimental - off by default)
+ - Rework hubzilla settings infrastructure
+ - Port the features to stand-alone apps
+ - Add app_destroy hook
+ - Improve mod network search
+ - Extend app_install() to allow installing by app name
+ - Remove tech levels
+ - Hide channel creation form when at or over service_class['limit_identities']
+ - Rename groups and group_members tables for MySQL 8 compatibility
+ - Improve checks for image magick and pdo at setup
+ - Allow a second url in apd files for settings
+ - Add contact autocomplete to mod photo comments
+ - Add hook to allow addons to filter the list returned by app_list
+ - Do not sync channel moved field
+ - Add attach_delete hook
+ - Catch errors in template rendering
+ - Provide a noscript_content switch for mod channel and display
+ - Install and update bootstrap via composer
+ - Improve cover-photo handling
+ - Improve notification handling on small screens
+ - Detect and automatically repair duplicate plugin hook scenarios
+ - Add dreport_process hook
+ - Redirect stdout/stderr on cron command
+ - Update composer libs and add ramsey/uuid
+ - Add hook to extend conv_item cog dropdown menu
+ - Trigger the query options off of the active module rather than passed parameters in first_post_date()
+ - Tweak archive widget for articles
+ - Add api_not_found hook
+ - Ignore deleted hublocs in zot finger
+ - Don't use "checkjs" with an associated page reload - wrap a static copy of the content in noscript tags instead
+ - Add possibility to override helpfiles
+ - Add support for overriding the default template location and individual templates via .htconfig.php
+ - Add table support to markdown
+ - Make channel_remove less memory hungry
+ - Prevent json-ld bombing
+ - Turn off browser autocomplete on channel sources creation
+ - Add alter_pdl hook
+ - Add ability for addons to create .pdl files and load them automatically
+ - Sanitise vcard fields
+ - Don't sync system apps
+
+
+ Bugfixes
+ - Fix issue with timeago plurals
+ - Fix issue with HTTP signatures
+ - Fix issues with channel import
+ - Fix double linebreaks in viewsrc output
+ - Fix jsonld signature issue (library is using sha1, spec requires sha256)
+ - Fix bookmarks not syncing between clones
+ - Fix combined view getting lost when deleting first message in pm thread
+ - Fix authors unable to comment on posts they authored when owned by others in certain circumstances
+ - Fix syschannel included in total channels count
+ - Fix html-to-markdown adds a backslash infront of a hash after each new line
+ - Fix profile likes dropdown
+ - Fix tags corruption when editing posts
+ - Fix duplicate info() messages
+ - Fix zid leaking to nonzot sites if markdown is enabled
+ - Fix app delete issue with base installed apps and app photo being reloaded uneccessarily
+ - Fix app update and ownership issues
+
+ Addons
+ - Upgrade Info: new addon to inform channel owners about system upgrades
+ - Superblock: fix issue with not removeable channels
+ - Cart: fix subscription table not created on install
+ - Hsse: new addon - a WYSIWYG editor for certain modules
+ - Rainbowtag: convert to app infrastructure
+ - Superblock: convert to app infrastructure
+ - Send ZID: convert to app infrastructure
+ - Adultphotoflag: move setting to mod photos
+ - GNU-Social: convert to app infrastructure
+ - Pubcrawl: convert to app infrastructure
+ - Startpage: convert to app infrastructure
+ - Wppost: convert to app infrastructure
+ - Diaspora: convert to app infrastructure
+ - Mdpost: move setting to editor settings
+ - Cart: convert to app infrastructure
+ - Cart: reflect renaming of groups table
+ - Authchoose: convert to app infrastructure
+ - Channelreputation: new addon - reputation system for community channels (forums, etc.)
+ - Diaspora: fix commenting on diaspora reshares
+ - Gallery: convert to app infrastructure
+ - Nsfw: convert to app infrastructure
+ - Diaspora: change top level retraction type from StatusMessage to Post
+ - Delivery Notice: new addon - display delivery status information at the top of items
+ - Diaspora: exclude xchan_networks rss, anon and unknown from the query to make the results more reliable
+ - Diaspora: provide xchan_url if we have no xchan_addr for mentions
+ - Diaspora: fix x-social-relay tags converted to associative array
+ - Twitter API: improvements for the twidere client
+ - Pubcrawl: partial support for inbound AP events
+ - Pubcrawl: add support for image objects
+ - Gallery: provide a way to direct link to a photo album gallery
+ - Pubcrawl: improve can_comment_on_post handler
+ - Pubcrawl: implement pleroma quirks regarding follow activities
+ - Cart: add ability to create catalog entries for physical and/or manually fulfilled items
+ - Cart: add subscriptions submodule
+
+
+Hubzilla 3.6 (2018-07-25)
- Update jquery.timeago library
- Implement Hookable CSP
- ActivityStreams: accept header changes to support plume
diff --git a/README.md b/README.md
index 69266f6ef..f3d159b1b 100644
--- a/README.md
+++ b/README.md
@@ -25,5 +25,8 @@ Hubzilla is completely decentralised and open source, for you modify or adapt to
The Hubzilla community consists of passionate volunteers creating an open source commons of decentralised services which are highly integrated and can rival the feature set of large centralised providers. We do our best to provide ethical software which places you in control of your online communications and privacy expectations.
+Build status master branch:
+[](https://framagit.org/hubzilla/core/badges/master/build.svg)
-[](https://travis-ci.org/redmatrix/hubzilla)
+Build status dev branch:
+[](https://framagit.org/hubzilla/core/badges/dev/build.svg)
diff --git a/Zotlabs/Daemon/Cron.php b/Zotlabs/Daemon/Cron.php
index d1c516f96..25e49b817 100644
--- a/Zotlabs/Daemon/Cron.php
+++ b/Zotlabs/Daemon/Cron.php
@@ -60,7 +60,7 @@ class Cron {
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.
- Zotlabs\Daemon\Master::Summon( [ 'Notifier', 'drop', $rr['id'] ] );
+ Master::Summon( [ 'Notifier', 'drop', $rr['id'] ] );
}
}
}
diff --git a/Zotlabs/Daemon/Master.php b/Zotlabs/Daemon/Master.php
index 580df97db..3a71ee578 100644
--- a/Zotlabs/Daemon/Master.php
+++ b/Zotlabs/Daemon/Master.php
@@ -3,7 +3,6 @@
namespace Zotlabs\Daemon;
if(array_search( __file__ , get_included_files()) === 0) {
-
require_once('include/cli_startup.php');
array_shift($argv);
$argc = count($argv);
@@ -17,14 +16,134 @@ if(array_search( __file__ , get_included_files()) === 0) {
class Master {
+ static public $queueworker = null;
+
static public function Summon($arr) {
proc_run('php','Zotlabs/Daemon/Master.php',$arr);
}
static public function Release($argc,$argv) {
cli_startup();
- logger('Master: release: ' . print_r($argv,true), LOGGER_ALL,LOG_DEBUG);
- $cls = '\\Zotlabs\\Daemon\\' . $argv[0];
- $cls::run($argc,$argv);
+
+ $maxworkers = get_config('system','max_queue_workers');
+
+ if (!$maxworkers || $maxworkers == 0) {
+ logger('Master: release: ' . print_r($argv,true), LOGGER_ALL,LOG_DEBUG);
+ $cls = '\\Zotlabs\\Daemon\\' . $argv[0];
+ $cls::run($argc,$argv);
+ self::ClearQueue();
+ } else {
+ logger('Master: enqueue: ' . print_r($argv,true), LOGGER_ALL,LOG_DEBUG);
+ $workinfo = ['argc'=>$argc,'argv'=>$argv];
+ q("insert into config (cat,k,v) values ('queuework','%s','%s')",
+ dbesc(uniqid('workitem:',true)),
+ dbesc(serialize($workinfo)));
+ self::Process();
+ }
+ }
+
+ static public function GetWorkerID() {
+ $maxworkers = get_config('system','max_queue_workers');
+ $maxworkers = ($maxworkers) ? $maxworkers : 3;
+
+ $workermaxage = get_config('system','max_queue_worker_age');
+ $workermaxage = ($workermaxage) ? $workermaxage : 300;
+
+ $workers = q("select * from config where cat='queueworkers' and k like '%s'", 'workerstarted_%');
+
+ if (count($workers) > $maxworkers) {
+ foreach ($workers as $idx => $worker) {
+ $curtime = time();
+ $age = (intval($curtime) - intval($worker['v']));
+ if ( $age > $workermaxage) {
+ logger("Prune worker: ".$worker['k'], LOGGER_ALL, LOGGER_DEBUG);
+ $k = explode('_',$worker['k']);
+ q("delete from config where cat='queueworkers' and k='%s'",
+ 'workerstarted_'.$k[1]);
+ q("update config set k='workitem' where cat='queuework' and k='%s'",
+ 'workitem_'.$k[1]);
+ unset($workers[$idx]);
+ }
+ }
+ if (count($workers) > $maxworkers) {
+ return false;
+ }
+ }
+ return uniqid();
+
+ }
+
+ static public function Process() {
+
+ self::$queueworker = self::GetWorkerID();
+
+ if (!self::$queueworker) {
+ logger('Master: unable to obtain worker ID.');
+ killme();
+ }
+
+ set_config('queueworkers','workerstarted_'.self::$queueworker,time());
+
+ $workersleep = get_config('system','queue_worker_sleep');
+ $workersleep = ($workersleep) ? $workersleep : 5;
+ cli_startup();
+
+ $work = q("update config set k='%s' where cat='queuework' and k like '%s' limit 1",
+ 'workitem_'.self::$queueworker,
+ dbesc('workitem:%'));
+ $jobs = 0;
+ while ($work) {
+ $workitem = q("select * from config where cat='queuework' and k='%s'",
+ 'workitem_'.self::$queueworker);
+
+ if (isset($workitem[0])) {
+ $jobs++;
+ $workinfo = unserialize($workitem[0]['v']);
+ $argc = $workinfo['argc'];
+ $argv = $workinfo['argv'];
+ logger('Master: process: ' . print_r($argv,true), LOGGER_ALL,LOG_DEBUG);
+
+ //Delete unclaimed duplicate workitems.
+ q("delete from config where cat='queuework' and k='workitem' and v='%s'",
+ serialize($argv));
+
+ $cls = '\\Zotlabs\\Daemon\\' . $argv[0];
+ $cls::run($argc,$argv);
+
+ //Right now we assume that if we get a return, everything is OK.
+ //At some point we may want to test whether the run returns true/false
+ // and requeue the work to be tried again. But we probably want
+ // to implement some sort of "retry interval" first.
+
+ q("delete from config where cat='queuework' and k='%s'",
+ 'workitem_'.self::$queueworker);
+ } else {
+ break;
+ }
+ sleep ($workersleep);
+ $work = q("update config set k='%s' where cat='queuework' and k like '%s' limit 1",
+ 'workitem_'.self::$queueworker,
+ dbesc('workitem:%'));
+
+ }
+ logger('Master: Worker Thread: queue items processed:' . $jobs);
+ q("delete from config where cat='queueworkers' and k='%s'",
+ 'workerstarted_'.self::$queueworker);
}
+
+ static public function ClearQueue() {
+ $work = q("select * from config where cat='queuework' and k like '%s'",
+ dbesc('workitem%'));
+ foreach ($work as $workitem) {
+ $workinfo = unserialize($workitem['v']);
+ $argc = $workinfo['argc'];
+ $argv = $workinfo['argv'];
+ logger('Master: process: ' . print_r($argv,true), LOGGER_ALL,LOG_DEBUG);
+ $cls = '\\Zotlabs\\Daemon\\' . $argv[0];
+ $cls::run($argc,$argv);
+ }
+ $work = q("delete from config where cat='queuework' and k like '%s'",
+ dbesc('workitem%'));
+ }
+
}
diff --git a/Zotlabs/Daemon/Notifier.php b/Zotlabs/Daemon/Notifier.php
index fa2368a92..f74c8f11c 100644
--- a/Zotlabs/Daemon/Notifier.php
+++ b/Zotlabs/Daemon/Notifier.php
@@ -559,6 +559,8 @@ class Notifier {
foreach($dhubs as $hub) {
+ logger('notifier_hub: ' . $hub['hubloc_url'],LOGGER_DEBUG);
+
if($hub['hubloc_network'] !== 'zot') {
$narr = [
'channel' => $channel,
diff --git a/Zotlabs/Extend/Route.php b/Zotlabs/Extend/Route.php
new file mode 100644
index 000000000..f7b90ec6e
--- /dev/null
+++ b/Zotlabs/Extend/Route.php
@@ -0,0 +1,48 @@
+addGrantType(new \OAuth2\GrantType\ClientCredentials($storage));
// Add the "Authorization Code" grant type (this is where the oauth magic happens)
- $this->addGrantType(new \OAuth2\GrantType\AuthorizationCode($storage));
+ // Need to use OpenID\GrantType to return id_token (see:https://github.com/bshaffer/oauth2-server-php/issues/443)
+ $this->addGrantType(new \OAuth2\OpenID\GrantType\AuthorizationCode($storage));
$keyStorage = new \OAuth2\Storage\Memory( [
'keys' => [
diff --git a/Zotlabs/Identity/OAuth2Storage.php b/Zotlabs/Identity/OAuth2Storage.php
index bc6db565c..bbf61cf2b 100644
--- a/Zotlabs/Identity/OAuth2Storage.php
+++ b/Zotlabs/Identity/OAuth2Storage.php
@@ -50,20 +50,78 @@ class OAuth2Storage extends \OAuth2\Storage\Pdo {
public function getUser($username)
{
- $x = channelx_by_nick($username);
+ $x = channelx_by_n($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( [
- 'username' => $x['channel_address'],
- 'user_id' => $x['channel_id'],
- 'firstName' => $x['channel_name'],
- 'lastName' => '',
- 'password' => 'NotARealPassword'
+ 'webfinger' => channel_reddress($x),
+ 'portable_id' => $x['channel_hash'],
+ 'email' => $a['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 = Array();
+ $claims = explode (' ', trim($claims));
+ $validclaims = Array ("name","preferred_username","webfinger","portable_id","email","picture","firstName","lastName");
+ $claimsmap = Array (
+ "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
*
@@ -78,4 +136,4 @@ class OAuth2Storage extends \OAuth2\Storage\Pdo {
return true;
}
-}
\ No newline at end of file
+}
diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php
new file mode 100644
index 000000000..6ddbbb9db
--- /dev/null
+++ b/Zotlabs/Lib/Activity.php
@@ -0,0 +1,1725 @@
+ 'Object',
+ 'id' => z_root() . '/thing/' . $r[0]['obj_obj'],
+ 'name' => $r[0]['obj_term']
+ ];
+
+ if($r[0]['obj_image'])
+ $x['image'] = $r[0]['obj_image'];
+
+ return $x;
+
+ }
+
+ static function fetch_item($x) {
+
+ if (array_key_exists('source',$x)) {
+ // This item is already processed and encoded
+ return $x;
+ }
+
+ $r = q("select * from item where mid = '%s' limit 1",
+ dbesc($x['id'])
+ );
+ if($r) {
+ xchan_query($r,true);
+ $r = fetch_post_tags($r,true);
+ return self::encode_item($r[0]);
+ }
+ }
+
+ static function encode_item_collection($items,$id,$type,$extra = null) {
+
+ $ret = [
+ 'id' => z_root() . '/' . $id,
+ 'type' => $type,
+ 'totalItems' => count($items),
+ ];
+ if($extra)
+ $ret = array_merge($ret,$extra);
+
+ if($items) {
+ $x = [];
+ foreach($items as $i) {
+ $t = self::encode_activity($i);
+ if($t)
+ $x[] = $t;
+ }
+ if($type === 'OrderedCollection')
+ $ret['orderedItems'] = $x;
+ else
+ $ret['items'] = $x;
+ }
+
+ return $ret;
+ }
+
+ static function encode_follow_collection($items,$id,$type,$extra = null) {
+
+ $ret = [
+ 'id' => z_root() . '/' . $id,
+ 'type' => $type,
+ 'totalItems' => count($items),
+ ];
+ if($extra)
+ $ret = array_merge($ret,$extra);
+
+ if($items) {
+ $x = [];
+ foreach($items as $i) {
+ if($i['xchan_url']) {
+ $x[] = $i['xchan_url'];
+ }
+ }
+
+ if($type === 'OrderedCollection')
+ $ret['orderedItems'] = $x;
+ else
+ $ret['items'] = $x;
+ }
+
+ return $ret;
+ }
+
+
+
+
+ static function encode_item($i) {
+
+ $ret = [];
+
+ $objtype = self::activity_obj_mapper($i['obj_type']);
+
+ if(intval($i['item_deleted'])) {
+ $ret['type'] = 'Tombstone';
+ $ret['formerType'] = $objtype;
+ $ret['id'] = ((strpos($i['mid'],'http') === 0) ? $i['mid'] : z_root() . '/item/' . urlencode($i['mid']));
+ return $ret;
+ }
+
+ $ret['type'] = $objtype;
+
+ $ret['id'] = ((strpos($i['mid'],'http') === 0) ? $i['mid'] : z_root() . '/item/' . urlencode($i['mid']));
+
+ if($i['title'])
+ $ret['title'] = bbcode($i['title']);
+
+ $ret['published'] = datetime_convert('UTC','UTC',$i['created'],ATOM_TIME);
+ if($i['created'] !== $i['edited'])
+ $ret['updated'] = datetime_convert('UTC','UTC',$i['edited'],ATOM_TIME);
+ if($i['app']) {
+ $ret['instrument'] = [ 'type' => 'Service', 'name' => $i['app'] ];
+ }
+ if($i['location'] || $i['coord']) {
+ $ret['location'] = [ 'type' => 'Place' ];
+ if($i['location']) {
+ $ret['location']['name'] = $i['location'];
+ }
+ if($i['coord']) {
+ $l = explode(' ',$i['coord']);
+ $ret['location']['latitude'] = $l[0];
+ $ret['location']['longitude'] = $l[1];
+ }
+ }
+
+ $ret['attributedTo'] = $i['author']['xchan_url'];
+
+ if($i['id'] != $i['parent']) {
+ $ret['inReplyTo'] = ((strpos($i['parent_mid'],'http') === 0) ? $i['parent_mid'] : z_root() . '/item/' . urlencode($i['parent_mid']));
+ }
+
+ if($i['mimetype'] === 'text/bbcode') {
+ if($i['title'])
+ $ret['name'] = bbcode($i['title']);
+ if($i['summary'])
+ $ret['summary'] = bbcode($i['summary']);
+ $ret['content'] = bbcode($i['body']);
+ $ret['source'] = [ 'content' => $i['body'], 'mediaType' => 'text/bbcode' ];
+ }
+
+ $actor = self::encode_person($i['author'],false);
+ if($actor)
+ $ret['actor'] = $actor;
+ else
+ return [];
+
+ $t = self::encode_taxonomy($i);
+ if($t) {
+ $ret['tag'] = $t;
+ }
+
+ $a = self::encode_attachment($i);
+ if($a) {
+ $ret['attachment'] = $a;
+ }
+
+ return $ret;
+ }
+
+ static function decode_taxonomy($item) {
+
+ $ret = [];
+
+ if($item['tag']) {
+ foreach($item['tag'] as $t) {
+ if(! array_key_exists('type',$t))
+ $t['type'] = 'Hashtag';
+
+ switch($t['type']) {
+ case 'Hashtag':
+ $ret[] = [ 'ttype' => TERM_HASHTAG, 'url' => $t['href'], 'term' => escape_tags((substr($t['name'],0,1) === '#') ? substr($t['name'],1) : $t['name']) ];
+ break;
+
+ case 'Mention':
+ $mention_type = substr($t['name'],0,1);
+ if($mention_type === '!') {
+ $ret[] = [ 'ttype' => TERM_FORUM, 'url' => $t['href'], 'term' => escape_tags(substr($t['name'],1)) ];
+ }
+ else {
+ $ret[] = [ 'ttype' => TERM_MENTION, 'url' => $t['href'], 'term' => escape_tags((substr($t['name'],0,1) === '@') ? substr($t['name'],1) : $t['name']) ];
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ return $ret;
+ }
+
+
+ static function encode_taxonomy($item) {
+
+ $ret = [];
+
+ if($item['term']) {
+ foreach($item['term'] as $t) {
+ switch($t['ttype']) {
+ case TERM_HASHTAG:
+ // An id is required so if we don't have a url in the taxonomy, ignore it and keep going.
+ if($t['url']) {
+ $ret[] = [ 'id' => $t['url'], 'name' => '#' . $t['term'] ];
+ }
+ break;
+
+ case TERM_FORUM:
+ $ret[] = [ 'type' => 'Mention', 'href' => $t['url'], 'name' => '!' . $t['term'] ];
+ break;
+
+ case TERM_MENTION:
+ $ret[] = [ 'type' => 'Mention', 'href' => $t['url'], 'name' => '@' . $t['term'] ];
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ return $ret;
+ }
+
+ static function encode_attachment($item) {
+
+ $ret = [];
+
+ if($item['attach']) {
+ $atts = json_decode($item['attach'],true);
+ if($atts) {
+ foreach($atts as $att) {
+ if(strpos($att['type'],'image')) {
+ $ret[] = [ 'type' => 'Image', 'url' => $att['href'] ];
+ }
+ else {
+ $ret[] = [ 'type' => 'Link', 'mediaType' => $att['type'], 'href' => $att['href'] ];
+ }
+ }
+ }
+ }
+
+ return $ret;
+ }
+
+
+ static function decode_attachment($item) {
+
+ $ret = [];
+
+ if($item['attachment']) {
+ foreach($item['attachment'] as $att) {
+ $entry = [];
+ if($att['href'])
+ $entry['href'] = $att['href'];
+ elseif($att['url'])
+ $entry['href'] = $att['url'];
+ if($att['mediaType'])
+ $entry['type'] = $att['mediaType'];
+ elseif($att['type'] === 'Image')
+ $entry['type'] = 'image/jpeg';
+ if($entry)
+ $ret[] = $entry;
+ }
+ }
+
+ return $ret;
+ }
+
+
+
+ static function encode_activity($i) {
+
+ $ret = [];
+ $reply = false;
+
+ if(intval($i['item_deleted'])) {
+ $ret['type'] = 'Tombstone';
+ $ret['formerType'] = self::activity_obj_mapper($i['obj_type']);
+ $ret['id'] = ((strpos($i['mid'],'http') === 0) ? $i['mid'] : z_root() . '/item/' . urlencode($i['mid']));
+ return $ret;
+ }
+
+ $ret['type'] = self::activity_mapper($i['verb']);
+ $ret['id'] = ((strpos($i['mid'],'http') === 0) ? $i['mid'] : z_root() . '/activity/' . urlencode($i['mid']));
+
+ if($i['title'])
+ $ret['name'] = html2plain(bbcode($i['title']));
+
+ if($i['summary'])
+ $ret['summary'] = bbcode($i['summary']);
+
+ if($ret['type'] === 'Announce') {
+ $tmp = preg_replace('/\[share(.*?)\[\/share\]/ism',EMPTY_STR, $i['body']);
+ $ret['content'] = bbcode($tmp);
+ $ret['source'] = [
+ 'content' => $i['body'],
+ 'mediaType' => 'text/bbcode'
+ ];
+ }
+
+ $ret['published'] = datetime_convert('UTC','UTC',$i['created'],ATOM_TIME);
+ if($i['created'] !== $i['edited'])
+ $ret['updated'] = datetime_convert('UTC','UTC',$i['edited'],ATOM_TIME);
+ if($i['app']) {
+ $ret['instrument'] = [ 'type' => 'Service', 'name' => $i['app'] ];
+ }
+ if($i['location'] || $i['coord']) {
+ $ret['location'] = [ 'type' => 'Place' ];
+ if($i['location']) {
+ $ret['location']['name'] = $i['location'];
+ }
+ if($i['coord']) {
+ $l = explode(' ',$i['coord']);
+ $ret['location']['latitude'] = $l[0];
+ $ret['location']['longitude'] = $l[1];
+ }
+ }
+
+ if($i['id'] != $i['parent']) {
+ $ret['inReplyTo'] = ((strpos($i['parent_mid'],'http') === 0) ? $i['parent_mid'] : z_root() . '/item/' . urlencode($i['parent_mid']));
+ $reply = true;
+
+ if($i['item_private']) {
+ $d = q("select xchan_url, xchan_addr, xchan_name from item left join xchan on xchan_hash = author_xchan where id = %d limit 1",
+ intval($i['parent'])
+ );
+ if($d) {
+ $is_directmessage = false;
+ $recips = get_iconfig($i['parent'], 'activitypub', 'recips');
+
+ if(in_array($i['author']['xchan_url'], $recips['to'])) {
+ $reply_url = $d[0]['xchan_url'];
+ $is_directmessage = true;
+ }
+ else {
+ $reply_url = z_root() . '/followers/' . substr($i['author']['xchan_addr'],0,strpos($i['author']['xchan_addr'],'@'));
+ }
+
+ $reply_addr = (($d[0]['xchan_addr']) ? $d[0]['xchan_addr'] : $d[0]['xchan_name']);
+ }
+ }
+
+ }
+
+ $actor = self::encode_person($i['author'],false);
+ if($actor)
+ $ret['actor'] = $actor;
+ else
+ return [];
+
+ if($i['obj']) {
+ if(! is_array($i['obj'])) {
+ $i['obj'] = json_decode($i['obj'],true);
+ }
+ $obj = self::encode_object($i['obj']);
+ if($obj)
+ $ret['object'] = $obj;
+ else
+ return [];
+ }
+ else {
+ $obj = self::encode_item($i);
+ if($obj)
+ $ret['object'] = $obj;
+ else
+ return [];
+ }
+
+ if($i['target']) {
+ if(! is_array($i['target'])) {
+ $i['target'] = json_decode($i['target'],true);
+ }
+ $tgt = self::encode_object($i['target']);
+ if($tgt)
+ $ret['target'] = $tgt;
+ else
+ return [];
+ }
+
+ return $ret;
+ }
+
+ static function map_mentions($i) {
+ if(! $i['term']) {
+ return [];
+ }
+
+ $list = [];
+
+ foreach ($i['term'] as $t) {
+ if($t['ttype'] == TERM_MENTION) {
+ $list[] = $t['url'];
+ }
+ }
+
+ return $list;
+ }
+
+ static function map_acl($i,$mentions = false) {
+
+ $private = false;
+ $list = [];
+ $x = collect_recipients($i,$private);
+ if($x) {
+ stringify_array_elms($x);
+ if(! $x)
+ return;
+
+ $strict = (($mentions) ? true : get_config('activitypub','compliance'));
+
+ $sql_extra = (($strict) ? " and xchan_network = 'activitypub' " : '');
+
+ $details = q("select xchan_url, xchan_addr, xchan_name from xchan where xchan_hash in (" . implode(',',$x) . ") $sql_extra");
+
+ if($details) {
+ foreach($details as $d) {
+ if($mentions) {
+ $list[] = [ 'type' => 'Mention', 'href' => $d['xchan_url'], 'name' => '@' . (($d['xchan_addr']) ? $d['xchan_addr'] : $d['xchan_name']) ];
+ }
+ else {
+ $list[] = $d['xchan_url'];
+ }
+ }
+ }
+ }
+
+ return $list;
+
+ }
+
+
+ static function encode_person($p, $extended = true) {
+
+ if(! $p['xchan_url'])
+ return [];
+
+ if(! $extended) {
+ return $p['xchan_url'];
+ }
+ $ret = [];
+
+ $ret['type'] = 'Person';
+ $ret['id'] = $p['xchan_url'];
+ if($p['xchan_addr'] && strpos($p['xchan_addr'],'@'))
+ $ret['preferredUsername'] = substr($p['xchan_addr'],0,strpos($p['xchan_addr'],'@'));
+ $ret['name'] = $p['xchan_name'];
+ $ret['updated'] = datetime_convert('UTC','UTC',$p['xchan_name_date'],ATOM_TIME);
+ $ret['icon'] = [
+ 'type' => 'Image',
+ 'mediaType' => (($p['xchan_photo_mimetype']) ? $p['xchan_photo_mimetype'] : 'image/png' ),
+ 'updated' => datetime_convert('UTC','UTC',$p['xchan_photo_date'],ATOM_TIME),
+ 'url' => $p['xchan_photo_l'],
+ 'height' => 300,
+ 'width' => 300,
+ ];
+ $ret['url'] = [
+ [
+ 'type' => 'Link',
+ 'mediaType' => 'text/html',
+ 'href' => $p['xchan_url']
+ ],
+ [
+ 'type' => 'Link',
+ 'mediaType' => 'text/x-zot+json',
+ 'href' => $p['xchan_url']
+ ]
+ ];
+
+ $arr = [ 'xchan' => $p, 'encoded' => $ret ];
+ call_hooks('encode_person', $arr);
+ $ret = $arr['encoded'];
+
+
+ return $ret;
+ }
+
+
+ static function activity_mapper($verb) {
+
+ if(strpos($verb,'/') === false) {
+ return $verb;
+ }
+
+ $acts = [
+ 'http://activitystrea.ms/schema/1.0/post' => 'Create',
+ 'http://activitystrea.ms/schema/1.0/share' => 'Announce',
+ 'http://activitystrea.ms/schema/1.0/update' => 'Update',
+ 'http://activitystrea.ms/schema/1.0/like' => 'Like',
+ 'http://activitystrea.ms/schema/1.0/favorite' => 'Like',
+ 'http://purl.org/zot/activity/dislike' => 'Dislike',
+ 'http://activitystrea.ms/schema/1.0/tag' => 'Add',
+ 'http://activitystrea.ms/schema/1.0/follow' => 'Follow',
+ 'http://activitystrea.ms/schema/1.0/unfollow' => 'Unfollow',
+ ];
+
+
+ if(array_key_exists($verb,$acts) && $acts[$verb]) {
+ return $acts[$verb];
+ }
+
+ // Reactions will just map to normal activities
+
+ if(strpos($verb,ACTIVITY_REACT) !== false)
+ return 'Create';
+ if(strpos($verb,ACTIVITY_MOOD) !== false)
+ return 'Create';
+
+ if(strpos($verb,ACTIVITY_POKE) !== false)
+ return 'Activity';
+
+ // We should return false, however this will trigger an uncaught execption and crash
+ // the delivery system if encountered by the JSON-LDSignature library
+
+ logger('Unmapped activity: ' . $verb);
+ return 'Create';
+ // return false;
+}
+
+
+ static function activity_obj_mapper($obj) {
+
+ if(strpos($obj,'/') === false) {
+ return $obj;
+ }
+
+ $objs = [
+ 'http://activitystrea.ms/schema/1.0/note' => 'Note',
+ 'http://activitystrea.ms/schema/1.0/comment' => 'Note',
+ 'http://activitystrea.ms/schema/1.0/person' => 'Person',
+ 'http://purl.org/zot/activity/profile' => 'Profile',
+ 'http://activitystrea.ms/schema/1.0/photo' => 'Image',
+ 'http://activitystrea.ms/schema/1.0/profile-photo' => 'Icon',
+ 'http://activitystrea.ms/schema/1.0/event' => 'Event',
+ 'http://activitystrea.ms/schema/1.0/wiki' => 'Document',
+ 'http://purl.org/zot/activity/location' => 'Place',
+ 'http://purl.org/zot/activity/chessgame' => 'Game',
+ 'http://purl.org/zot/activity/tagterm' => 'zot:Tag',
+ 'http://purl.org/zot/activity/thing' => 'Object',
+ 'http://purl.org/zot/activity/file' => 'zot:File',
+ 'http://purl.org/zot/activity/mood' => 'zot:Mood',
+
+ ];
+
+ if(array_key_exists($obj,$objs)) {
+ return $objs[$obj];
+ }
+
+ logger('Unmapped activity object: ' . $obj);
+ return 'Note';
+
+ // return false;
+
+ }
+
+
+ static function follow($channel,$act) {
+
+ $contact = null;
+ $their_follow_id = null;
+
+ /*
+ *
+ * if $act->type === 'Follow', actor is now following $channel
+ * if $act->type === 'Accept', actor has approved a follow request from $channel
+ *
+ */
+
+ $person_obj = $act->actor;
+
+ if($act->type === 'Follow') {
+ $their_follow_id = $act->id;
+ }
+ elseif($act->type === 'Accept') {
+ $my_follow_id = z_root() . '/follow/' . $contact['id'];
+ }
+
+ if(is_array($person_obj)) {
+
+ // store their xchan and hubloc
+
+ self::actor_store($person_obj['id'],$person_obj);
+
+ // Find any existing abook record
+
+ $r = q("select * from abook left join xchan on abook_xchan = xchan_hash where abook_xchan = '%s' and abook_channel = %d limit 1",
+ dbesc($person_obj['id']),
+ intval($channel['channel_id'])
+ );
+ if($r) {
+ $contact = $r[0];
+ }
+ }
+
+ $x = \Zotlabs\Access\PermissionRoles::role_perms('social');
+ $p = \Zotlabs\Access\Permissions::FilledPerms($x['perms_connect']);
+ $their_perms = \Zotlabs\Access\Permissions::serialise($p);
+
+ if($contact && $contact['abook_id']) {
+
+ // A relationship of some form already exists on this site.
+
+ switch($act->type) {
+
+ case 'Follow':
+
+ // A second Follow request, but we haven't approved the first one
+
+ if($contact['abook_pending']) {
+ return;
+ }
+
+ // We've already approved them or followed them first
+ // Send an Accept back to them
+
+ set_abconfig($channel['channel_id'],$person_obj['id'],'pubcrawl','their_follow_id', $their_follow_id);
+ \Zotlabs\Daemon\Master::Summon([ 'Notifier', 'permissions_accept', $contact['abook_id'] ]);
+ return;
+
+ case 'Accept':
+
+ // They accepted our Follow request - set default permissions
+
+ set_abconfig($channel['channel_id'],$contact['abook_xchan'],'system','their_perms',$their_perms);
+
+ $abook_instance = $contact['abook_instance'];
+
+ if(strpos($abook_instance,z_root()) === false) {
+ if($abook_instance)
+ $abook_instance .= ',';
+ $abook_instance .= z_root();
+
+ $r = q("update abook set abook_instance = '%s', abook_not_here = 0
+ where abook_id = %d and abook_channel = %d",
+ dbesc($abook_instance),
+ intval($contact['abook_id']),
+ intval($channel['channel_id'])
+ );
+ }
+
+ return;
+ default:
+ return;
+
+ }
+ }
+
+ // No previous relationship exists.
+
+ if($act->type === 'Accept') {
+ // This should not happen unless we deleted the connection before it was accepted.
+ return;
+ }
+
+ // From here on out we assume a Follow activity to somebody we have no existing relationship with
+
+ set_abconfig($channel['channel_id'],$person_obj['id'],'pubcrawl','their_follow_id', $their_follow_id);
+
+ // The xchan should have been created by actor_store() above
+
+ $r = q("select * from xchan where xchan_hash = '%s' and xchan_network = 'activitypub' limit 1",
+ dbesc($person_obj['id'])
+ );
+
+ if(! $r) {
+ logger('xchan not found for ' . $person_obj['id']);
+ return;
+ }
+ $ret = $r[0];
+
+ $p = \Zotlabs\Access\Permissions::connect_perms($channel['channel_id']);
+ $my_perms = \Zotlabs\Access\Permissions::serialise($p['perms']);
+ $automatic = $p['automatic'];
+
+ $closeness = get_pconfig($channel['channel_id'],'system','new_abook_closeness',80);
+
+ $r = abook_store_lowlevel(
+ [
+ 'abook_account' => intval($channel['channel_account_id']),
+ 'abook_channel' => intval($channel['channel_id']),
+ 'abook_xchan' => $ret['xchan_hash'],
+ 'abook_closeness' => intval($closeness),
+ 'abook_created' => datetime_convert(),
+ 'abook_updated' => datetime_convert(),
+ 'abook_connected' => datetime_convert(),
+ 'abook_dob' => NULL_DATE,
+ 'abook_pending' => intval(($automatic) ? 0 : 1),
+ 'abook_instance' => z_root()
+ ]
+ );
+
+ if($my_perms)
+ set_abconfig($channel['channel_id'],$ret['xchan_hash'],'system','my_perms',$my_perms);
+
+ if($their_perms)
+ set_abconfig($channel['channel_id'],$ret['xchan_hash'],'system','their_perms',$their_perms);
+
+
+ if($r) {
+ logger("New ActivityPub follower for {$channel['channel_name']}");
+
+ $new_connection = q("select * from abook left join xchan on abook_xchan = xchan_hash left join hubloc on hubloc_hash = xchan_hash where abook_channel = %d and abook_xchan = '%s' order by abook_created desc limit 1",
+ intval($channel['channel_id']),
+ dbesc($ret['xchan_hash'])
+ );
+ if($new_connection) {
+ \Zotlabs\Lib\Enotify::submit(
+ [
+ 'type' => NOTIFY_INTRO,
+ 'from_xchan' => $ret['xchan_hash'],
+ 'to_xchan' => $channel['channel_hash'],
+ 'link' => z_root() . '/connedit/' . $new_connection[0]['abook_id'],
+ ]
+ );
+
+ if($my_perms && $automatic) {
+ // send an Accept for this Follow activity
+ \Zotlabs\Daemon\Master::Summon([ 'Notifier', 'permissions_accept', $new_connection[0]['abook_id'] ]);
+ // Send back a Follow notification to them
+ \Zotlabs\Daemon\Master::Summon([ 'Notifier', 'permissions_create', $new_connection[0]['abook_id'] ]);
+ }
+
+ $clone = array();
+ foreach($new_connection[0] as $k => $v) {
+ if(strpos($k,'abook_') === 0) {
+ $clone[$k] = $v;
+ }
+ }
+ unset($clone['abook_id']);
+ unset($clone['abook_account']);
+ unset($clone['abook_channel']);
+
+ $abconfig = load_abconfig($channel['channel_id'],$clone['abook_xchan']);
+
+ if($abconfig)
+ $clone['abconfig'] = $abconfig;
+
+ Libsync::build_sync_packet($channel['channel_id'], [ 'abook' => array($clone) ] );
+ }
+ }
+
+
+ /* If there is a default group for this channel and permissions are automatic, add this member to it */
+
+ if($channel['channel_default_group'] && $automatic) {
+ $g = Group::rec_byhash($channel['channel_id'],$channel['channel_default_group']);
+ if($g)
+ Group::member_add($channel['channel_id'],'',$ret['xchan_hash'],$g['id']);
+ }
+
+
+ return;
+
+ }
+
+
+ static function unfollow($channel,$act) {
+
+ $contact = null;
+
+ /* @FIXME This really needs to be a signed request. */
+
+ /* actor is unfollowing $channel */
+
+ $person_obj = $act->actor;
+
+ if(is_array($person_obj)) {
+
+ $r = q("select * from abook left join xchan on abook_xchan = xchan_hash where abook_xchan = '%s' and abook_channel = %d limit 1",
+ dbesc($person_obj['id']),
+ intval($channel['channel_id'])
+ );
+ if($r) {
+ // remove all permissions they provided
+ del_abconfig($channel['channel_id'],$r[0]['xchan_hash'],'system','their_perms',EMPTY_STR);
+ }
+ }
+
+ return;
+ }
+
+
+
+
+ static function actor_store($url,$person_obj) {
+
+ if(! is_array($person_obj))
+ return;
+
+ $name = $person_obj['name'];
+ if(! $name)
+ $name = $person_obj['preferredUsername'];
+ if(! $name)
+ $name = t('Unknown');
+
+ if($person_obj['icon']) {
+ if(is_array($person_obj['icon'])) {
+ if(array_key_exists('url',$person_obj['icon']))
+ $icon = $person_obj['icon']['url'];
+ else
+ $icon = $person_obj['icon'][0]['url'];
+ }
+ else
+ $icon = $person_obj['icon'];
+ }
+
+ if(is_array($person_obj['url']) && array_key_exists('href', $person_obj['url']))
+ $profile = $person_obj['url']['href'];
+ else
+ $profile = $url;
+
+
+ $inbox = $person_obj['inbox'];
+
+ $collections = [];
+
+ if($inbox) {
+ $collections['inbox'] = $inbox;
+ if($person_obj['outbox'])
+ $collections['outbox'] = $person_obj['outbox'];
+ if($person_obj['followers'])
+ $collections['followers'] = $person_obj['followers'];
+ if($person_obj['following'])
+ $collections['following'] = $person_obj['following'];
+ if($person_obj['endpoints'] && $person_obj['endpoints']['sharedInbox'])
+ $collections['sharedInbox'] = $person_obj['endpoints']['sharedInbox'];
+ }
+
+ if(array_key_exists('publicKey',$person_obj) && array_key_exists('publicKeyPem',$person_obj['publicKey'])) {
+ if($person_obj['id'] === $person_obj['publicKey']['owner']) {
+ $pubkey = $person_obj['publicKey']['publicKeyPem'];
+ if(strstr($pubkey,'RSA ')) {
+ $pubkey = rsatopem($pubkey);
+ }
+ }
+ }
+
+ $r = q("select * from xchan where xchan_hash = '%s' limit 1",
+ dbesc($url)
+ );
+ if(! $r) {
+ // create a new record
+ $r = xchan_store_lowlevel(
+ [
+ 'xchan_hash' => $url,
+ 'xchan_guid' => $url,
+ 'xchan_pubkey' => $pubkey,
+ 'xchan_addr' => '',
+ 'xchan_url' => $profile,
+ 'xchan_name' => $name,
+ 'xchan_name_date' => datetime_convert(),
+ 'xchan_network' => 'activitypub'
+ ]
+ );
+ }
+ else {
+
+ // Record exists. Cache existing records for one week at most
+ // then refetch to catch updated profile photos, names, etc.
+
+ $d = datetime_convert('UTC','UTC','now - 1 week');
+ if($r[0]['xchan_name_date'] > $d)
+ return;
+
+ // update existing record
+ $r = q("update xchan set xchan_name = '%s', xchan_pubkey = '%s', xchan_network = '%s', xchan_name_date = '%s' where xchan_hash = '%s'",
+ dbesc($name),
+ dbesc($pubkey),
+ dbesc('activitypub'),
+ dbesc(datetime_convert()),
+ dbesc($url)
+ );
+ }
+
+ if($collections) {
+ set_xconfig($url,'activitypub','collections',$collections);
+ }
+
+ $r = q("select * from hubloc where hubloc_hash = '%s' limit 1",
+ dbesc($url)
+ );
+
+
+ $m = parse_url($url);
+ if($m) {
+ $hostname = $m['host'];
+ $baseurl = $m['scheme'] . '://' . $m['host'] . (($m['port']) ? ':' . $m['port'] : '');
+ }
+
+ if(! $r) {
+ $r = hubloc_store_lowlevel(
+ [
+ 'hubloc_guid' => $url,
+ 'hubloc_hash' => $url,
+ 'hubloc_addr' => '',
+ 'hubloc_network' => 'activitypub',
+ 'hubloc_url' => $baseurl,
+ 'hubloc_host' => $hostname,
+ 'hubloc_callback' => $inbox,
+ 'hubloc_updated' => datetime_convert(),
+ 'hubloc_primary' => 1
+ ]
+ );
+ }
+
+ if(! $icon)
+ $icon = z_root() . '/' . get_default_profile_photo(300);
+
+ $photos = import_xchan_photo($icon,$url);
+ $r = q("update xchan set xchan_photo_date = '%s', xchan_photo_l = '%s', xchan_photo_m = '%s', xchan_photo_s = '%s', xchan_photo_mimetype = '%s' where xchan_hash = '%s'",
+ dbescdate(datetime_convert('UTC','UTC',$arr['photo_updated'])),
+ dbesc($photos[0]),
+ dbesc($photos[1]),
+ dbesc($photos[2]),
+ dbesc($photos[3]),
+ dbesc($url)
+ );
+
+ }
+
+
+ static function create_action($channel,$observer_hash,$act) {
+
+ if(in_array($act->obj['type'], [ 'Note', 'Article', 'Video' ])) {
+ self::create_note($channel,$observer_hash,$act);
+ }
+
+
+ }
+
+ static function announce_action($channel,$observer_hash,$act) {
+
+ if(in_array($act->type, [ 'Announce' ])) {
+ self::announce_note($channel,$observer_hash,$act);
+ }
+
+ }
+
+
+ static function like_action($channel,$observer_hash,$act) {
+
+ if(in_array($act->obj['type'], [ 'Note', 'Article', 'Video' ])) {
+ self::like_note($channel,$observer_hash,$act);
+ }
+
+
+ }
+
+ // sort function width decreasing
+
+ static function as_vid_sort($a,$b) {
+ if($a['width'] === $b['width'])
+ return 0;
+ return (($a['width'] > $b['width']) ? -1 : 1);
+ }
+
+ static function create_note($channel,$observer_hash,$act) {
+
+ $s = [];
+
+ // Mastodon only allows visibility in public timelines if the public inbox is listed in the 'to' field.
+ // They are hidden in the public timeline if the public inbox is listed in the 'cc' field.
+ // This is not part of the activitypub protocol - we might change this to show all public posts in pubstream at some point.
+ $pubstream = ((is_array($act->obj) && array_key_exists('to', $act->obj) && in_array(ACTIVITY_PUBLIC_INBOX, $act->obj['to'])) ? true : false);
+ $is_sys_channel = is_sys_channel($channel['channel_id']);
+
+ $parent = ((array_key_exists('inReplyTo',$act->obj)) ? urldecode($act->obj['inReplyTo']) : '');
+ if($parent) {
+
+ $r = q("select * from item where uid = %d and ( mid = '%s' or mid = '%s' ) limit 1",
+ intval($channel['channel_id']),
+ dbesc($parent),
+ dbesc(basename($parent))
+ );
+
+ if(! $r) {
+ logger('parent not found.');
+ return;
+ }
+
+ if($r[0]['owner_xchan'] === $channel['channel_hash']) {
+ if(! perm_is_allowed($channel['channel_id'],$observer_hash,'send_stream') && ! ($is_sys_channel && $pubstream)) {
+ logger('no comment permission.');
+ return;
+ }
+ }
+
+ $s['parent_mid'] = $r[0]['mid'];
+ $s['owner_xchan'] = $r[0]['owner_xchan'];
+ $s['author_xchan'] = $observer_hash;
+
+ }
+ else {
+ if(! perm_is_allowed($channel['channel_id'],$observer_hash,'send_stream') && ! ($is_sys_channel && $pubstream)) {
+ logger('no permission');
+ return;
+ }
+ $s['owner_xchan'] = $s['author_xchan'] = $observer_hash;
+ }
+
+ $abook = q("select * from abook where abook_xchan = '%s' and abook_channel = %d limit 1",
+ dbesc($observer_hash),
+ intval($channel['channel_id'])
+ );
+
+ $content = self::get_content($act->obj);
+
+ if(! $content) {
+ logger('no content');
+ return;
+ }
+
+ $s['aid'] = $channel['channel_account_id'];
+ $s['uid'] = $channel['channel_id'];
+ $s['mid'] = urldecode($act->obj['id']);
+ $s['plink'] = urldecode($act->obj['id']);
+
+
+ if($act->data['published']) {
+ $s['created'] = datetime_convert('UTC','UTC',$act->data['published']);
+ }
+ elseif($act->obj['published']) {
+ $s['created'] = datetime_convert('UTC','UTC',$act->obj['published']);
+ }
+ if($act->data['updated']) {
+ $s['edited'] = datetime_convert('UTC','UTC',$act->data['updated']);
+ }
+ elseif($act->obj['updated']) {
+ $s['edited'] = datetime_convert('UTC','UTC',$act->obj['updated']);
+ }
+
+ if(! $s['created'])
+ $s['created'] = datetime_convert();
+
+ if(! $s['edited'])
+ $s['edited'] = $s['created'];
+
+
+ if(! $s['parent_mid'])
+ $s['parent_mid'] = $s['mid'];
+
+
+ $s['title'] = self::bb_content($content,'name');
+ $s['summary'] = self::bb_content($content,'summary');
+ $s['body'] = self::bb_content($content,'content');
+ $s['verb'] = ACTIVITY_POST;
+ $s['obj_type'] = ACTIVITY_OBJ_NOTE;
+
+ $instrument = $act->get_property_obj('instrument');
+ if(! $instrument)
+ $instrument = $act->get_property_obj('instrument',$act->obj);
+
+ if($instrument && array_key_exists('type',$instrument)
+ && $instrument['type'] === 'Service' && array_key_exists('name',$instrument)) {
+ $s['app'] = escape_tags($instrument['name']);
+ }
+
+ if($channel['channel_system']) {
+ if(! \Zotlabs\Lib\MessageFilter::evaluate($s,get_config('system','pubstream_incl'),get_config('system','pubstream_excl'))) {
+ logger('post is filtered');
+ return;
+ }
+ }
+
+
+ if($abook) {
+ if(! post_is_importable($s,$abook[0])) {
+ logger('post is filtered');
+ return;
+ }
+ }
+
+ if($act->obj['conversation']) {
+ set_iconfig($s,'ostatus','conversation',$act->obj['conversation'],1);
+ }
+
+ $a = self::decode_taxonomy($act->obj);
+ if($a) {
+ $s['term'] = $a;
+ }
+
+ $a = self::decode_attachment($act->obj);
+ if($a) {
+ $s['attach'] = $a;
+ }
+
+ if($act->obj['type'] === 'Note' && $s['attach']) {
+ $s['body'] .= self::bb_attach($s['attach']);
+ }
+
+ // we will need a hook here to extract magnet links e.g. peertube
+ // right now just link to the largest mp4 we find that will fit in our
+ // standard content region
+
+ if($act->obj['type'] === 'Video') {
+
+ $vtypes = [
+ 'video/mp4',
+ 'video/ogg',
+ 'video/webm'
+ ];
+
+ $mps = [];
+ if(array_key_exists('url',$act->obj) && is_array($act->obj['url'])) {
+ foreach($act->obj['url'] as $vurl) {
+ if(in_array($vurl['mimeType'], $vtypes)) {
+ if(! array_key_exists('width',$vurl)) {
+ $vurl['width'] = 0;
+ }
+ $mps[] = $vurl;
+ }
+ }
+ }
+ if($mps) {
+ usort($mps,'as_vid_sort');
+ foreach($mps as $m) {
+ if(intval($m['width']) < 500) {
+ $s['body'] .= "\n\n" . '[video]' . $m['href'] . '[/video]';
+ break;
+ }
+ }
+ }
+ }
+
+ if($act->recips && (! in_array(ACTIVITY_PUBLIC_INBOX,$act->recips)))
+ $s['item_private'] = 1;
+
+ set_iconfig($s,'activitypub','recips',$act->raw_recips);
+ if($parent) {
+ set_iconfig($s,'activitypub','rawmsg',$act->raw,1);
+ }
+
+ $x = null;
+
+ $r = q("select created, edited from item where mid = '%s' and uid = %d limit 1",
+ dbesc($s['mid']),
+ intval($s['uid'])
+ );
+ if($r) {
+ if($s['edited'] > $r[0]['edited']) {
+ $x = item_store_update($s);
+ }
+ else {
+ return;
+ }
+ }
+ else {
+ $x = item_store($s);
+ }
+
+ if(is_array($x) && $x['item_id']) {
+ if($parent) {
+ if($s['owner_xchan'] === $channel['channel_hash']) {
+ // We are the owner of this conversation, so send all received comments back downstream
+ Zotlabs\Daemon\Master::Summon(array('Notifier','comment-import',$x['item_id']));
+ }
+ $r = q("select * from item where id = %d limit 1",
+ intval($x['item_id'])
+ );
+ if($r) {
+ send_status_notifications($x['item_id'],$r[0]);
+ }
+ }
+ sync_an_item($channel['channel_id'],$x['item_id']);
+ }
+
+ }
+
+
+ static function decode_note($act) {
+
+ $s = [];
+
+
+
+ $content = self::get_content($act->obj);
+
+ $s['owner_xchan'] = $act->actor['id'];
+ $s['author_xchan'] = $act->actor['id'];
+
+ $s['mid'] = $act->id;
+ $s['parent_mid'] = $act->parent_id;
+
+
+ if($act->data['published']) {
+ $s['created'] = datetime_convert('UTC','UTC',$act->data['published']);
+ }
+ elseif($act->obj['published']) {
+ $s['created'] = datetime_convert('UTC','UTC',$act->obj['published']);
+ }
+ if($act->data['updated']) {
+ $s['edited'] = datetime_convert('UTC','UTC',$act->data['updated']);
+ }
+ elseif($act->obj['updated']) {
+ $s['edited'] = datetime_convert('UTC','UTC',$act->obj['updated']);
+ }
+
+ if(! $s['created'])
+ $s['created'] = datetime_convert();
+
+ if(! $s['edited'])
+ $s['edited'] = $s['created'];
+
+ if(in_array($act->type,['Announce'])) {
+ $root_content = self::get_content($act->raw);
+
+ $s['title'] = self::bb_content($root_content,'name');
+ $s['summary'] = self::bb_content($root_content,'summary');
+ $s['body'] = (self::bb_content($root_content,'bbcode') ? : self::bb_content($root_content,'content'));
+
+ if(strpos($s['body'],'[share') === false) {
+
+ // @fixme - error check and set defaults
+
+ $name = urlencode($act->obj['actor']['name']);
+ $profile = $act->obj['actor']['id'];
+ $photo = $act->obj['icon']['url'];
+
+ $s['body'] .= "\r\n[share author='" . $name .
+ "' profile='" . $profile .
+ "' avatar='" . $photo .
+ "' link='" . $act->obj['id'] .
+ "' auth='" . ((is_matrix_url($act->obj['id'])) ? 'true' : 'false' ) .
+ "' posted='" . $act->obj['published'] .
+ "' message_id='" . $act->obj['id'] .
+ "']";
+ }
+ }
+ else {
+ $s['title'] = self::bb_content($content,'name');
+ $s['summary'] = self::bb_content($content,'summary');
+ $s['body'] = (self::bb_content($content,'bbcode') ? : self::bb_content($content,'content'));
+ }
+
+ $s['verb'] = self::activity_mapper($act->type);
+
+ if($act->type === 'Tombstone') {
+ $s['item_deleted'] = 1;
+ }
+
+ $s['obj_type'] = self::activity_obj_mapper($act->obj['type']);
+ $s['obj'] = $act->obj;
+
+ $instrument = $act->get_property_obj('instrument');
+ if(! $instrument)
+ $instrument = $act->get_property_obj('instrument',$act->obj);
+
+ if($instrument && array_key_exists('type',$instrument)
+ && $instrument['type'] === 'Service' && array_key_exists('name',$instrument)) {
+ $s['app'] = escape_tags($instrument['name']);
+ }
+
+ $a = self::decode_taxonomy($act->obj);
+ if($a) {
+ $s['term'] = $a;
+ }
+
+ $a = self::decode_attachment($act->obj);
+ if($a) {
+ $s['attach'] = $a;
+ }
+
+ // we will need a hook here to extract magnet links e.g. peertube
+ // right now just link to the largest mp4 we find that will fit in our
+ // standard content region
+
+ if($act->obj['type'] === 'Video') {
+
+ $vtypes = [
+ 'video/mp4',
+ 'video/ogg',
+ 'video/webm'
+ ];
+
+ $mps = [];
+ if(array_key_exists('url',$act->obj) && is_array($act->obj['url'])) {
+ foreach($act->obj['url'] as $vurl) {
+ if(in_array($vurl['mimeType'], $vtypes)) {
+ if(! array_key_exists('width',$vurl)) {
+ $vurl['width'] = 0;
+ }
+ $mps[] = $vurl;
+ }
+ }
+ }
+ if($mps) {
+ usort($mps,'as_vid_sort');
+ foreach($mps as $m) {
+ if(intval($m['width']) < 500) {
+ $s['body'] .= "\n\n" . '[video]' . $m['href'] . '[/video]';
+ break;
+ }
+ }
+ }
+ }
+
+ if($act->recips && (! in_array(ACTIVITY_PUBLIC_INBOX,$act->recips)))
+ $s['item_private'] = 1;
+
+ set_iconfig($s,'activitypub','recips',$act->raw_recips);
+
+ if($parent) {
+ set_iconfig($s,'activitypub','rawmsg',$act->raw,1);
+ }
+
+ return $s;
+
+ }
+
+
+
+ static function announce_note($channel,$observer_hash,$act) {
+
+ $s = [];
+
+ $is_sys_channel = is_sys_channel($channel['channel_id']);
+
+ // Mastodon only allows visibility in public timelines if the public inbox is listed in the 'to' field.
+ // They are hidden in the public timeline if the public inbox is listed in the 'cc' field.
+ // This is not part of the activitypub protocol - we might change this to show all public posts in pubstream at some point.
+ $pubstream = ((is_array($act->obj) && array_key_exists('to', $act->obj) && in_array(ACTIVITY_PUBLIC_INBOX, $act->obj['to'])) ? true : false);
+
+ if(! perm_is_allowed($channel['channel_id'],$observer_hash,'send_stream') && ! ($is_sys_channel && $pubstream)) {
+ logger('no permission');
+ return;
+ }
+
+ $content = self::get_content($act->obj);
+
+ if(! $content) {
+ logger('no content');
+ return;
+ }
+
+ $s['owner_xchan'] = $s['author_xchan'] = $observer_hash;
+
+ $s['aid'] = $channel['channel_account_id'];
+ $s['uid'] = $channel['channel_id'];
+ $s['mid'] = urldecode($act->obj['id']);
+ $s['plink'] = urldecode($act->obj['id']);
+
+ if(! $s['created'])
+ $s['created'] = datetime_convert();
+
+ if(! $s['edited'])
+ $s['edited'] = $s['created'];
+
+
+ $s['parent_mid'] = $s['mid'];
+
+ $s['verb'] = ACTIVITY_POST;
+ $s['obj_type'] = ACTIVITY_OBJ_NOTE;
+ $s['app'] = t('ActivityPub');
+
+ if($channel['channel_system']) {
+ if(! \Zotlabs\Lib\MessageFilter::evaluate($s,get_config('system','pubstream_incl'),get_config('system','pubstream_excl'))) {
+ logger('post is filtered');
+ return;
+ }
+ }
+
+ $abook = q("select * from abook where abook_xchan = '%s' and abook_channel = %d limit 1",
+ dbesc($observer_hash),
+ intval($channel['channel_id'])
+ );
+
+ if($abook) {
+ if(! post_is_importable($s,$abook[0])) {
+ logger('post is filtered');
+ return;
+ }
+ }
+
+ if($act->obj['conversation']) {
+ set_iconfig($s,'ostatus','conversation',$act->obj['conversation'],1);
+ }
+
+ $a = self::decode_taxonomy($act->obj);
+ if($a) {
+ $s['term'] = $a;
+ }
+
+ $a = self::decode_attachment($act->obj);
+ if($a) {
+ $s['attach'] = $a;
+ }
+
+ $body = "[share author='" . urlencode($act->sharee['name']) .
+ "' profile='" . $act->sharee['url'] .
+ "' avatar='" . $act->sharee['photo_s'] .
+ "' link='" . ((is_array($act->obj['url'])) ? $act->obj['url']['href'] : $act->obj['url']) .
+ "' auth='" . ((is_matrix_url($act->obj['url'])) ? 'true' : 'false' ) .
+ "' posted='" . $act->obj['published'] .
+ "' message_id='" . $act->obj['id'] .
+ "']";
+
+ if($content['name'])
+ $body .= self::bb_content($content,'name') . "\r\n";
+
+ $body .= self::bb_content($content,'content');
+
+ if($act->obj['type'] === 'Note' && $s['attach']) {
+ $body .= self::bb_attach($s['attach']);
+ }
+
+ $body .= "[/share]";
+
+ $s['title'] = self::bb_content($content,'name');
+ $s['body'] = $body;
+
+ if($act->recips && (! in_array(ACTIVITY_PUBLIC_INBOX,$act->recips)))
+ $s['item_private'] = 1;
+
+ set_iconfig($s,'activitypub','recips',$act->raw_recips);
+
+ $r = q("select created, edited from item where mid = '%s' and uid = %d limit 1",
+ dbesc($s['mid']),
+ intval($s['uid'])
+ );
+ if($r) {
+ if($s['edited'] > $r[0]['edited']) {
+ $x = item_store_update($s);
+ }
+ else {
+ return;
+ }
+ }
+ else {
+ $x = item_store($s);
+ }
+
+
+ if(is_array($x) && $x['item_id']) {
+ if($parent) {
+ if($s['owner_xchan'] === $channel['channel_hash']) {
+ // We are the owner of this conversation, so send all received comments back downstream
+ Zotlabs\Daemon\Master::Summon(array('Notifier','comment-import',$x['item_id']));
+ }
+ $r = q("select * from item where id = %d limit 1",
+ intval($x['item_id'])
+ );
+ if($r) {
+ send_status_notifications($x['item_id'],$r[0]);
+ }
+ }
+ sync_an_item($channel['channel_id'],$x['item_id']);
+ }
+
+
+ }
+
+ static function like_note($channel,$observer_hash,$act) {
+
+ $s = [];
+
+ $parent = $act->obj['id'];
+
+ if($act->type === 'Like')
+ $s['verb'] = ACTIVITY_LIKE;
+ if($act->type === 'Dislike')
+ $s['verb'] = ACTIVITY_DISLIKE;
+
+ if(! $parent)
+ return;
+
+ $r = q("select * from item where uid = %d and ( mid = '%s' or mid = '%s' ) limit 1",
+ intval($channel['channel_id']),
+ dbesc($parent),
+ dbesc(urldecode(basename($parent)))
+ );
+
+ if(! $r) {
+ logger('parent not found.');
+ return;
+ }
+
+ xchan_query($r);
+ $parent_item = $r[0];
+
+ if($parent_item['owner_xchan'] === $channel['channel_hash']) {
+ if(! perm_is_allowed($channel['channel_id'],$observer_hash,'post_comments')) {
+ logger('no comment permission.');
+ return;
+ }
+ }
+
+ if($parent_item['mid'] === $parent_item['parent_mid']) {
+ $s['parent_mid'] = $parent_item['mid'];
+ }
+ else {
+ $s['thr_parent'] = $parent_item['mid'];
+ $s['parent_mid'] = $parent_item['parent_mid'];
+ }
+
+ $s['owner_xchan'] = $parent_item['owner_xchan'];
+ $s['author_xchan'] = $observer_hash;
+
+ $s['aid'] = $channel['channel_account_id'];
+ $s['uid'] = $channel['channel_id'];
+ $s['mid'] = $act->id;
+
+ if(! $s['parent_mid'])
+ $s['parent_mid'] = $s['mid'];
+
+
+ $post_type = (($parent_item['resource_type'] === 'photo') ? t('photo') : t('status'));
+
+ $links = array(array('rel' => 'alternate','type' => 'text/html', 'href' => $parent_item['plink']));
+ $objtype = (($parent_item['resource_type'] === 'photo') ? ACTIVITY_OBJ_PHOTO : ACTIVITY_OBJ_NOTE );
+
+ $body = $parent_item['body'];
+
+ $z = q("select * from xchan where xchan_hash = '%s' limit 1",
+ dbesc($parent_item['author_xchan'])
+ );
+ if($z)
+ $item_author = $z[0];
+
+ $object = json_encode(array(
+ 'type' => $post_type,
+ 'id' => $parent_item['mid'],
+ 'parent' => (($parent_item['thr_parent']) ? $parent_item['thr_parent'] : $parent_item['parent_mid']),
+ 'link' => $links,
+ 'title' => $parent_item['title'],
+ 'content' => $parent_item['body'],
+ 'created' => $parent_item['created'],
+ 'edited' => $parent_item['edited'],
+ 'author' => array(
+ 'name' => $item_author['xchan_name'],
+ 'address' => $item_author['xchan_addr'],
+ 'guid' => $item_author['xchan_guid'],
+ 'guid_sig' => $item_author['xchan_guid_sig'],
+ 'link' => array(
+ array('rel' => 'alternate', 'type' => 'text/html', 'href' => $item_author['xchan_url']),
+ array('rel' => 'photo', 'type' => $item_author['xchan_photo_mimetype'], 'href' => $item_author['xchan_photo_m'])),
+ ),
+ ), JSON_UNESCAPED_SLASHES
+ );
+
+ if($act->type === 'Like')
+ $bodyverb = t('%1$s likes %2$s\'s %3$s');
+ if($act->type === 'Dislike')
+ $bodyverb = t('%1$s doesn\'t like %2$s\'s %3$s');
+
+ $ulink = '[url=' . $item_author['xchan_url'] . ']' . $item_author['xchan_name'] . '[/url]';
+ $alink = '[url=' . $parent_item['author']['xchan_url'] . ']' . $parent_item['author']['xchan_name'] . '[/url]';
+ $plink = '[url='. z_root() . '/display/' . urlencode($act->id) . ']' . $post_type . '[/url]';
+ $s['body'] = sprintf( $bodyverb, $ulink, $alink, $plink );
+
+ $s['app'] = t('ActivityPub');
+
+ // set the route to that of the parent so downstream hubs won't reject it.
+
+ $s['route'] = $parent_item['route'];
+ $s['item_private'] = $parent_item['item_private'];
+ $s['obj_type'] = $objtype;
+ $s['obj'] = $object;
+
+ if($act->obj['conversation']) {
+ set_iconfig($s,'ostatus','conversation',$act->obj['conversation'],1);
+ }
+
+ if($act->recips && (! in_array(ACTIVITY_PUBLIC_INBOX,$act->recips)))
+ $s['item_private'] = 1;
+
+ set_iconfig($s,'activitypub','recips',$act->raw_recips);
+
+ $result = item_store($s);
+
+ if($result['success']) {
+ // if the message isn't already being relayed, notify others
+ if(intval($parent_item['item_origin']))
+ Zotlabs\Daemon\Master::Summon(array('Notifier','comment-import',$result['item_id']));
+ sync_an_item($channel['channel_id'],$result['item_id']);
+ }
+
+ return;
+ }
+
+
+ static function bb_attach($attach) {
+
+ $ret = false;
+
+ foreach($attach as $a) {
+ if(strpos($a['type'],'image') !== false) {
+ $ret .= "\n\n" . '[img]' . $a['href'] . '[/img]';
+ }
+ if(array_key_exists('type',$a) && strpos($a['type'], 'video') === 0) {
+ $ret .= "\n\n" . '[video]' . $a['href'] . '[/video]';
+ }
+ if(array_key_exists('type',$a) && strpos($a['type'], 'audio') === 0) {
+ $ret .= "\n\n" . '[audio]' . $a['href'] . '[/audio]';
+ }
+ }
+
+ return $ret;
+ }
+
+
+
+ static function bb_content($content,$field) {
+
+ require_once('include/html2bbcode.php');
+
+ $ret = false;
+
+ if(is_array($content[$field])) {
+ foreach($content[$field] as $k => $v) {
+ $ret .= '[language=' . $k . ']' . html2bbcode($v) . '[/language]';
+ }
+ }
+ else {
+ if($field === 'bbcode' && array_key_exists('bbcode',$content)) {
+ $ret = $content[$field];
+ }
+ else {
+ $ret = html2bbcode($content[$field]);
+ }
+ }
+
+ return $ret;
+ }
+
+
+ static function get_content($act) {
+
+ $content = [];
+ if (! $act) {
+ return $content;
+ }
+
+ foreach ([ 'name', 'summary', 'content' ] as $a) {
+ if (($x = self::get_textfield($act,$a)) !== false) {
+ $content[$a] = $x;
+ }
+ }
+ if (array_key_exists('source',$act) && array_key_exists('mediaType',$act['source'])) {
+ if ($act['source']['mediaType'] === 'text/bbcode') {
+ $content['bbcode'] = purify_html($act['source']['content']);
+ }
+ }
+
+ return $content;
+ }
+
+
+ static function get_textfield($act,$field) {
+
+ $content = false;
+
+ if(array_key_exists($field,$act) && $act[$field])
+ $content = purify_html($act[$field]);
+ elseif(array_key_exists($field . 'Map',$act) && $act[$field . 'Map']) {
+ foreach($act[$field . 'Map'] as $k => $v) {
+ $content[escape_tags($k)] = purify_html($v);
+ }
+ }
+ return $content;
+ }
+}
\ No newline at end of file
diff --git a/Zotlabs/Lib/Apps.php b/Zotlabs/Lib/Apps.php
index a966842ae..b13658be2 100644
--- a/Zotlabs/Lib/Apps.php
+++ b/Zotlabs/Lib/Apps.php
@@ -56,15 +56,10 @@ class Apps {
}
-
- static public function import_system_apps() {
- if(! local_channel())
- return;
-
- self::$base_apps = get_config('system','base_apps',[
+ static public function get_base_apps() {
+ return get_config('system','base_apps',[
'Connections',
- 'Suggest Channels',
- 'Grid',
+ 'Network',
'Settings',
'Files',
'Channel Home',
@@ -77,7 +72,14 @@ class Apps {
'Mail',
'Profile Photo'
]);
+ }
+ static public function import_system_apps() {
+ if(! local_channel())
+ return;
+
+ self::$base_apps = self::get_base_apps();
+
$apps = self::get_system_apps(false);
self::$available_apps = q("select * from app where app_channel = 0");
@@ -266,6 +268,10 @@ class Apps {
if(! can_view_public_stream())
unset($ret);
break;
+ case 'custom_role':
+ if(get_pconfig(local_channel(),'system','permissions_role') !== 'custom')
+ unset($ret);
+ break;
case 'observer':
if(! $observer)
unset($ret);
@@ -297,14 +303,14 @@ class Apps {
'Cards' => t('Cards'),
'Admin' => t('Site Admin'),
'Report Bug' => t('Report Bug'),
- 'View Bookmarks' => t('View Bookmarks'),
- 'My Chatrooms' => t('My Chatrooms'),
+ 'Bookmarks' => t('Bookmarks'),
+ 'Chatrooms' => t('Chatrooms'),
'Connections' => t('Connections'),
'Remote Diagnostics' => t('Remote Diagnostics'),
'Suggest Channels' => t('Suggest Channels'),
'Login' => t('Login'),
'Channel Manager' => t('Channel Manager'),
- 'Grid' => t('Activity'),
+ 'Network' => t('Stream'),
'Settings' => t('Settings'),
'Files' => t('Files'),
'Webpages' => t('Webpages'),
@@ -332,7 +338,20 @@ class Apps {
'Profiles' => t('Profiles'),
'Privacy Groups' => t('Privacy Groups'),
'Notifications' => t('Notifications'),
- 'Order Apps' => t('Order Apps')
+ 'Order Apps' => t('Order Apps'),
+ 'CalDAV' => t('CalDAV'),
+ 'CardDAV' => t('CardDAV'),
+ 'Channel Sources' => t('Channel Sources'),
+ 'Guest Access' => t('Guest Access'),
+ 'Notes' => t('Notes'),
+ 'OAuth Apps Manager' => t('OAuth Apps Manager'),
+ 'OAuth2 Apps Manager' => t('OAuth2 Apps Manager'),
+ 'PDL Editor' => t('PDL Editor'),
+ 'Permission Categories' => t('Permission Categories'),
+ 'Premium Channel' => t('Premium Channel'),
+ 'Public Stream' => t('Public Stream'),
+ 'My Chatrooms' => t('My Chatrooms'),
+ 'Channel Export' => t('Channel Export')
);
if(array_key_exists('name',$arr)) {
@@ -344,6 +363,9 @@ class Apps {
for($x = 0; $x < count($arr); $x++) {
if(array_key_exists($arr[$x]['name'],$apps)) {
$arr[$x]['name'] = $apps[$arr[$x]['name']];
+ } else {
+ // Try to guess by app name if not in list
+ $arr[$x]['name'] = t(trim($arr[$x]['name']));
}
}
}
@@ -383,18 +405,23 @@ class Apps {
// This will catch somebody clicking on a system "available" app that hasn't had the path macros replaced
// and they are allowed to see the app
-
-
- if(strstr($papp['url'],'$baseurl') || strstr($papp['url'],'$nick') || strstr($papp['photo'],'$baseurl') || strstr($pap['photo'],'$nick')) {
+ if(strpos($papp['url'],'$baseurl') !== false || strpos($papp['url'],'$nick') !== false || strpos($papp['photo'],'$baseurl') !== false || strpos($papp['photo'],'$nick') !== false) {
$view_channel = local_channel();
if(! $view_channel) {
+
$sys = get_sys_channel();
$view_channel = $sys['channel_id'];
}
self::app_macros($view_channel,$papp);
}
- if(! strstr($papp['url'],'://'))
+ if(strpos($papp['url'], ',')) {
+ $urls = explode(',', $papp['url']);
+ $papp['url'] = trim($urls[0]);
+ $papp['settings_url'] = trim($urls[1]);
+ }
+
+ if(! strpos($papp['url'],'://'))
$papp['url'] = z_root() . ((strpos($papp['url'],'/') === 0) ? '' : '/') . $papp['url'];
@@ -442,6 +469,10 @@ class Apps {
if(! can_view_public_stream())
return '';
break;
+ case 'custom_role':
+ if(get_pconfig(local_channel(),'system','permissions_role') != 'custom')
+ return '';
+ break;
case 'observer':
$observer = \App::get_observer();
if(! $observer)
@@ -463,7 +494,9 @@ class Apps {
$hosturl = '';
if(local_channel()) {
- $installed = self::app_installed(local_channel(),$papp);
+ if(self::app_installed(local_channel(),$papp) && !$papp['deleted'])
+ $installed = true;
+
$hosturl = z_root() . '/';
}
elseif(remote_channel()) {
@@ -490,18 +523,21 @@ class Apps {
if($mode === 'install') {
$papp['embed'] = true;
}
+
return replace_macros(get_markup_template('app.tpl'),array(
'$app' => $papp,
'$icon' => $icon,
'$hosturl' => $hosturl,
'$purchase' => (($papp['page'] && (! $installed)) ? t('Purchase') : ''),
- '$install' => (($hosturl && in_array($mode, ['view','install'])) ? $install_action : ''),
+ '$installed' => $installed,
+ '$action_label' => (($hosturl && in_array($mode, ['view','install'])) ? $install_action : ''),
'$edit' => ((local_channel() && $installed && $mode == 'edit') ? t('Edit') : ''),
- '$delete' => ((local_channel() && $installed && $mode == 'edit') ? t('Delete') : ''),
- '$undelete' => ((local_channel() && $installed && $mode == 'edit') ? t('Undelete') : ''),
+ '$delete' => ((local_channel() && $mode == 'edit') ? t('Delete') : ''),
+ '$undelete' => ((local_channel() && $mode == 'edit') ? t('Undelete') : ''),
+ '$settings_url' => ((local_channel() && $installed && $mode == 'list') ? $papp['settings_url'] : ''),
'$deleted' => $papp['deleted'],
- '$feature' => (($papp['embed']) ? false : true),
- '$pin' => (($papp['embed']) ? false : true),
+ '$feature' => (($papp['embed'] || $mode == 'edit') ? false : true),
+ '$pin' => (($papp['embed'] || $mode == 'edit') ? false : true),
'$featured' => ((strpos($papp['categories'], 'nav_featured_app') === false) ? false : true),
'$pinned' => ((strpos($papp['categories'], 'nav_pinned_app') === false) ? false : true),
'$navapps' => (($mode == 'nav') ? true : false),
@@ -509,14 +545,26 @@ class Apps {
'$add' => t('Add to app-tray'),
'$remove' => t('Remove from app-tray'),
'$add_nav' => t('Pin to navbar'),
- '$remove_nav' => t('Unpin from navbar')
+ '$remove_nav' => t('Unpin from navbar'),
+ '$rpath' => z_root() . '/apps'
));
}
static public function app_install($uid,$app) {
+
+ if(! is_array($app)) {
+ $r = q("select * from app where app_name = '%s' and app_channel = 0",
+ dbesc($app)
+ );
+ if(! $r)
+ return false;
+
+ $app = self::app_encode($r[0]);
+ }
+
$app['uid'] = $uid;
- if(self::app_installed($uid,$app))
+ if(self::app_installed($uid,$app,true))
$x = self::app_update($app);
else
$x = self::app_store($app);
@@ -527,7 +575,7 @@ class Apps {
intval($uid)
);
if($r) {
- if(! $r[0]['app_system']) {
+ if(($app['uid']) && (! $r[0]['app_system'])) {
if($app['categories'] && (! $app['term'])) {
$r[0]['term'] = q("select * from term where otype = %d and oid = %d",
intval(TERM_OBJ_APP),
@@ -542,8 +590,25 @@ class Apps {
return false;
}
- static public function app_destroy($uid,$app) {
+ static public function can_delete($uid,$app) {
+ if(! $uid) {
+ return false;
+ }
+
+ $base_apps = self::get_base_apps();
+ if($base_apps) {
+ foreach($base_apps as $b) {
+ if($app['guid'] === hash('whirlpool',$b)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+
+ static public function app_destroy($uid,$app) {
if($uid && $app['guid']) {
@@ -554,23 +619,33 @@ class Apps {
if($x) {
if(! intval($x[0]['app_deleted'])) {
$x[0]['app_deleted'] = 1;
- q("delete from term where otype = %d and oid = %d",
- intval(TERM_OBJ_APP),
- intval($x[0]['id'])
- );
- $r = q("delete from app where app_id = '%s' and app_channel = %d",
- dbesc($app['guid']),
- intval($uid)
- );
-
- // we don't sync system apps - they may be completely different on the other system
- build_sync_packet($uid,array('app' => $x));
+ if(self::can_delete($uid,$app)) {
+ $r = q("delete from app where app_id = '%s' and app_channel = %d",
+ dbesc($app['guid']),
+ intval($uid)
+ );
+ q("delete from term where otype = %d and oid = %d",
+ intval(TERM_OBJ_APP),
+ intval($x[0]['id'])
+ );
+ call_hooks('app_destroy', $x[0]);
+ }
+ else {
+ $r = q("update app set app_deleted = 1 where app_id = '%s' and app_channel = %d",
+ dbesc($app['guid']),
+ intval($uid)
+ );
+ }
+ if(! intval($x[0]['app_system'])) {
+ build_sync_packet($uid,array('app' => $x));
+ }
}
else {
self::app_undestroy($uid,$app);
}
}
}
+
}
static public function app_undestroy($uid,$app) {
@@ -618,17 +693,66 @@ class Apps {
}
}
- static public function app_installed($uid,$app) {
+ static public function app_installed($uid,$app,$bypass_filter=false) {
$r = q("select id from app where app_id = '%s' and app_channel = %d limit 1",
dbesc((array_key_exists('guid',$app)) ? $app['guid'] : ''),
intval($uid)
);
+ if (!$bypass_filter) {
+ $filter_arr = [
+ 'uid'=>$uid,
+ 'app'=>$app,
+ 'installed'=>$r
+ ];
+ call_hooks('app_installed_filter',$filter_arr);
+ $r = $filter_arr['installed'];
+ }
return(($r) ? true : false);
}
+ static public function addon_app_installed($uid,$app,$bypass_filter=false) {
+
+ $r = q("select id from app where app_plugin = '%s' and app_channel = %d limit 1",
+ dbesc($app),
+ intval($uid)
+ );
+ if (!$bypass_filter) {
+ $filter_arr = [
+ 'uid'=>$uid,
+ 'app'=>$app,
+ 'installed'=>$r
+ ];
+ call_hooks('addon_app_installed_filter',$filter_arr);
+ $r = $filter_arr['installed'];
+ }
+ return(($r) ? true : false);
+
+ }
+
+ static public function system_app_installed($uid,$app,$bypass_filter=false) {
+
+ $r = q("select id from app where app_id = '%s' and app_channel = %d limit 1",
+ dbesc(hash('whirlpool',$app)),
+ intval($uid)
+ );
+ if (!$bypass_filter) {
+ $filter_arr = [
+ 'uid'=>$uid,
+ 'app'=>$app,
+ 'installed'=>$r
+ ];
+ call_hooks('system_app_installed_filter',$filter_arr);
+ $r = $filter_arr['installed'];
+ }
+ return(($r) ? true : false);
+
+ }
+
+
+
static public function app_list($uid, $deleted = false, $cats = []) {
if($deleted)
$sql_extra = "";
@@ -668,6 +792,9 @@ class Apps {
);
if($r) {
+ $hookinfo = Array('uid'=>$uid,'deleted'=>$deleted,'cats'=>$cats,'apps'=>$r);
+ call_hooks('app_list',$hookinfo);
+ $r = $hookinfo['apps'];
for($x = 0; $x < count($r); $x ++) {
if(! $r[$x]['app_system'])
$r[$x]['type'] = 'personal';
@@ -855,8 +982,8 @@ class Apps {
$arr['author'] = $sys['channel_hash'];
}
- if($arr['photo'] && (strpos($arr['photo'],'icon:') !== 0) && (! strstr($arr['photo'],z_root()))) {
- $x = import_xchan_photo($arr['photo'],get_observer_hash(),true);
+ if($arr['photo'] && (strpos($arr['photo'],'icon:') === false) && (strpos($arr['photo'],z_root()) !== false)) {
+ $x = import_xchan_photo(str_replace('$baseurl',z_root(),$arr['photo']),get_observer_hash(),true);
$arr['photo'] = $x[1];
}
@@ -875,10 +1002,11 @@ class Apps {
$darray['app_requires'] = ((x($arr,'requires')) ? escape_tags($arr['requires']) : '');
$darray['app_system'] = ((x($arr,'system')) ? intval($arr['system']) : 0);
$darray['app_deleted'] = ((x($arr,'deleted')) ? intval($arr['deleted']) : 0);
+ $darray['app_options'] = ((x($arr,'options')) ? intval($arr['options']) : 0);
$created = datetime_convert();
- $r = q("insert into app ( app_id, app_sig, app_author, app_name, app_desc, app_url, app_photo, app_version, app_channel, app_addr, app_price, app_page, app_requires, app_created, app_edited, app_system, app_plugin, app_deleted ) values ( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, '%s', '%s', '%s', '%s', '%s', '%s', %d, '%s', %d )",
+ $r = q("insert into app ( app_id, app_sig, app_author, app_name, app_desc, app_url, app_photo, app_version, app_channel, app_addr, app_price, app_page, app_requires, app_created, app_edited, app_system, app_plugin, app_deleted, app_options ) values ( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, '%s', '%s', '%s', '%s', '%s', '%s', %d, '%s', %d, %d )",
dbesc($darray['app_id']),
dbesc($darray['app_sig']),
dbesc($darray['app_author']),
@@ -896,7 +1024,8 @@ class Apps {
dbesc($created),
intval($darray['app_system']),
dbesc($darray['app_plugin']),
- intval($darray['app_deleted'])
+ intval($darray['app_deleted']),
+ intval($darray['app_options'])
);
if($r) {
@@ -939,8 +1068,8 @@ class Apps {
if((! $darray['app_url']) || (! $darray['app_id']))
return $ret;
- if($arr['photo'] && (strpos($arr['photo'],'icon:') !== 0) && (! strstr($arr['photo'],z_root()))) {
- $x = import_xchan_photo($arr['photo'],get_observer_hash(),true);
+ if($arr['photo'] && (strpos($arr['photo'],'icon:') === false) && (strpos($arr['photo'],z_root()) !== false)) {
+ $x = import_xchan_photo(str_replace('$baseurl',z_root(),$arr['photo']),get_observer_hash(),true);
$arr['photo'] = $x[1];
}
@@ -957,10 +1086,11 @@ class Apps {
$darray['app_requires'] = ((x($arr,'requires')) ? escape_tags($arr['requires']) : '');
$darray['app_system'] = ((x($arr,'system')) ? intval($arr['system']) : 0);
$darray['app_deleted'] = ((x($arr,'deleted')) ? intval($arr['deleted']) : 0);
+ $darray['app_options'] = ((x($arr,'options')) ? intval($arr['options']) : 0);
$edited = datetime_convert();
- $r = q("update app set app_sig = '%s', app_author = '%s', app_name = '%s', app_desc = '%s', app_url = '%s', app_photo = '%s', app_version = '%s', app_addr = '%s', app_price = '%s', app_page = '%s', app_requires = '%s', app_edited = '%s', app_system = %d, app_plugin = '%s', app_deleted = %d where app_id = '%s' and app_channel = %d",
+ $r = q("update app set app_sig = '%s', app_author = '%s', app_name = '%s', app_desc = '%s', app_url = '%s', app_photo = '%s', app_version = '%s', app_addr = '%s', app_price = '%s', app_page = '%s', app_requires = '%s', app_edited = '%s', app_system = %d, app_plugin = '%s', app_deleted = %d, app_options = %d where app_id = '%s' and app_channel = %d",
dbesc($darray['app_sig']),
dbesc($darray['app_author']),
dbesc($darray['app_name']),
@@ -976,6 +1106,7 @@ class Apps {
intval($darray['app_system']),
dbesc($darray['app_plugin']),
intval($darray['app_deleted']),
+ intval($darray['app_options']),
dbesc($darray['app_id']),
intval($darray['app_channel'])
);
@@ -1065,6 +1196,9 @@ class Apps {
if($app['app_system'])
$ret['system'] = $app['app_system'];
+ if($app['app_options'])
+ $ret['options'] = $app['app_options'];
+
if($app['app_plugin'])
$ret['plugin'] = trim($app['app_plugin']);
diff --git a/Zotlabs/Lib/Group.php b/Zotlabs/Lib/Group.php
new file mode 100644
index 000000000..a4ff4fced
--- /dev/null
+++ b/Zotlabs/Lib/Group.php
@@ -0,0 +1,405 @@
+may apply to this group and any future members. If this is not what you intended, please create another group with a different name.') . EOL);
+ }
+ return true;
+ }
+
+ do {
+ $dups = false;
+ $hash = random_string(32) . str_replace(['<','>'],['.','.'], $name);
+
+ $r = q("SELECT id FROM pgrp WHERE hash = '%s' LIMIT 1", dbesc($hash));
+ if($r)
+ $dups = true;
+ } while($dups == true);
+
+
+ $r = q("INSERT INTO pgrp ( hash, uid, visible, gname )
+ VALUES( '%s', %d, %d, '%s' ) ",
+ dbesc($hash),
+ intval($uid),
+ intval($public),
+ dbesc($name)
+ );
+ $ret = $r;
+ }
+
+ Libsync::build_sync_packet($uid,null,true);
+ return $ret;
+ }
+
+
+ static function remove($uid,$name) {
+ $ret = false;
+ if(x($uid) && x($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'];
+ }
+
+ if(! $group_id)
+ return false;
+
+ // remove group from default posting lists
+ $r = q("SELECT channel_default_group, channel_allow_gid, channel_deny_gid FROM channel WHERE channel_id = %d LIMIT 1",
+ intval($uid)
+ );
+ if($r) {
+ $user_info = $r[0];
+ $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;
+ }
+
+
+ static function byname($uid,$name) {
+ if((! $uid) || (! strlen($name)))
+ return false;
+ $r = q("SELECT * FROM pgrp WHERE uid = %d AND gname = '%s' LIMIT 1",
+ intval($uid),
+ dbesc($name)
+ );
+ if($r)
+ return $r[0]['id'];
+ return false;
+ }
+
+
+ static function rec_byhash($uid,$hash) {
+ if((! $uid) || (! strlen($hash)))
+ return false;
+ $r = q("SELECT * FROM pgrp WHERE uid = %d AND hash = '%s' LIMIT 1",
+ intval($uid),
+ dbesc($hash)
+ );
+ if($r)
+ return $r[0];
+ return false;
+ }
+
+
+ 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;
+ }
+
+
+ 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
+ if(! $r)
+ $r = q("INSERT INTO pgrp_member (uid, gid, xchan)
+ VALUES( %d, %d, '%s' ) ",
+ intval($uid),
+ intval($gid),
+ dbesc($member)
+ );
+
+ Libsync::build_sync_packet($uid,null,true);
+
+ return $r;
+ }
+
+
+ static function members($gid) {
+ $ret = array();
+ if(intval($gid)) {
+ $r = q("SELECT * FROM pgrp_member
+ LEFT JOIN abook ON abook_xchan = pgrp_member.xchan left join xchan on xchan_hash = abook_xchan
+ WHERE gid = %d AND abook_channel = %d and pgrp_member.uid = %d and xchan_deleted = 0 and abook_self = 0 and abook_blocked = 0 and abook_pending = 0 ORDER BY xchan_name ASC ",
+ intval($gid),
+ intval(local_channel()),
+ intval(local_channel())
+ );
+ if($r)
+ $ret = $r;
+ }
+ return $ret;
+ }
+
+ static function members_xchan($gid) {
+ $ret = [];
+ if(intval($gid)) {
+ $r = q("SELECT xchan FROM pgrp_member WHERE gid = %d AND uid = %d",
+ intval($gid),
+ intval(local_channel())
+ );
+ if($r) {
+ foreach($r as $rr) {
+ $ret[] = $rr['xchan'];
+ }
+ }
+ }
+ return $ret;
+ }
+
+ static function members_profile_xchan($uid,$gid) {
+ $ret = [];
+
+ if(intval($gid)) {
+ $r = q("SELECT abook_xchan as xchan from abook left join profile on abook_profile = profile_guid where profile.id = %d and profile.uid = %d",
+ intval($gid),
+ intval($uid)
+ );
+ if($r) {
+ foreach($r as $rr) {
+ $ret[] = $rr['xchan'];
+ }
+ }
+ }
+ return $ret;
+ }
+
+
+
+
+ static function select($uid,$group = '') {
+
+ $grps = [];
+ $o = '';
+
+ $r = q("SELECT * FROM pgrp WHERE deleted = 0 AND uid = %d ORDER BY gname ASC",
+ intval($uid)
+ );
+ $grps[] = array('name' => '', 'hash' => '0', 'selected' => '');
+ if($r) {
+ foreach($r as $rr) {
+ $grps[] = array('name' => $rr['gname'], 'id' => $rr['hash'], 'selected' => (($group == $rr['hash']) ? 'true' : ''));
+ }
+
+ }
+ logger('select: ' . print_r($grps,true), LOGGER_DATA);
+
+ $o = replace_macros(get_markup_template('group_selection.tpl'), array(
+ '$label' => t('Add new connections to this privacy group'),
+ '$groups' => $grps
+ ));
+ return $o;
+ }
+
+
+
+
+ static function widget($every="connections",$each="group",$edit = false, $group_id = 0, $cid = '',$mode = 1) {
+
+ $o = '';
+
+ if(! (local_channel() && feature_enabled(local_channel(),'groups'))) {
+ return '';
+ }
+
+ $groups = array();
+
+ $r = q("SELECT * FROM pgrp WHERE deleted = 0 AND uid = %d ORDER BY gname ASC",
+ intval($_SESSION['uid'])
+ );
+ $member_of = array();
+ 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' => "group/".$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),
+ ];
+ }
+ }
+
+
+ $tpl = get_markup_template("group_side.tpl");
+ $o = replace_macros($tpl, array(
+ '$title' => t('Privacy Groups'),
+ '$edittext' => t('Edit group'),
+ '$createtext' => t('Add privacy group'),
+ '$ungrouped' => (($every === 'contacts') ? t('Channels not in any privacy group') : ''),
+ '$groups' => $groups,
+ '$add' => t('add'),
+ ));
+
+
+ return $o;
+ }
+
+
+ static function expand($g) {
+ if(! (is_array($g) && count($g)))
+ return array();
+
+ $ret = [];
+ $x = [];
+
+ // private profile linked virtual groups
+
+ foreach($g as $gv) {
+ if(substr($gv,0,3) === 'vp.') {
+ $profile_hash = substr($gv,3);
+ if($profile_hash) {
+ $r = q("select abook_xchan from abook where abook_profile = '%s'",
+ dbesc($profile_hash)
+ );
+ if($r) {
+ foreach($r as $rv) {
+ $ret[] = $rv['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 $rr) {
+ $ret[] = $rr['xchan'];
+ }
+ }
+ }
+ }
+ return $ret;
+ }
+
+
+ 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;
+
+ }
+
+ 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 = array();
+ if($r) {
+ foreach($r as $rr)
+ $ret[] = $rr['gid'];
+ }
+
+ return $ret;
+ }
+}
\ No newline at end of file
diff --git a/Zotlabs/Lib/Libsync.php b/Zotlabs/Lib/Libsync.php
new file mode 100644
index 000000000..d037a0058
--- /dev/null
+++ b/Zotlabs/Lib/Libsync.php
@@ -0,0 +1,1019 @@
+ $channel['channel_address'], 'url' => z_root() ];
+
+ if(array_key_exists($uid,\App::$config) && array_key_exists('transient',\App::$config[$uid])) {
+ $settings = \App::$config[$uid]['transient'];
+ if($settings) {
+ $info['config'] = $settings;
+ }
+ }
+
+ if($channel) {
+ $info['channel'] = array();
+ foreach($channel as $k => $v) {
+
+ // filter out any joined tables like xchan
+
+ if(strpos($k,'channel_') !== 0)
+ continue;
+
+ // don't pass these elements, they should not be synchronised
+
+
+ $disallowed = [
+ 'channel_id','channel_account_id','channel_primary','channel_address',
+ 'channel_deleted','channel_removed','channel_system'
+ ];
+
+ if(! $keychange) {
+ $disallowed[] = 'channel_prvkey';
+ }
+
+ if(in_array($k,$disallowed))
+ continue;
+
+ $info['channel'][$k] = $v;
+ }
+ }
+
+ if($groups_changed) {
+ $r = q("select hash as collection, visible, deleted, gname as name from pgrp where uid = %d",
+ intval($uid)
+ );
+ if($r)
+ $info['collections'] = $r;
+
+ $r = q("select pgrp.hash as collection, pgrp_member.xchan as member from pgrp left join pgrp_member on pgrp.id = pgrp_member.gid where pgrp_member.uid = %d",
+ intval($uid)
+ );
+ if($r)
+ $info['collection_members'] = $r;
+ }
+
+ $interval = ((get_config('system','delivery_interval') !== false)
+ ? intval(get_config('system','delivery_interval')) : 2 );
+
+ logger('Packet: ' . print_r($info,true), LOGGER_DATA, LOG_DEBUG);
+
+ $total = count($synchubs);
+
+ foreach($synchubs as $hub) {
+ $hash = random_string();
+ $n = Libzot::build_packet($channel,'sync',$env_recips,json_encode($info),'red',$hub['hubloc_sitekey'],$hub['site_crypto']);
+ Queue::insert(array(
+ 'hash' => $hash,
+ 'account_id' => $channel['channel_account_id'],
+ 'channel_id' => $channel['channel_id'],
+ 'posturl' => $hub['hubloc_callback'],
+ 'notify' => $n,
+ 'msg' => EMPTY_STR
+ ));
+
+
+ $x = q("select count(outq_hash) as total from outq where outq_delivered = 0");
+ if(intval($x[0]['total']) > intval(get_config('system','force_queue_threshold',3000))) {
+ logger('immediate delivery deferred.', LOGGER_DEBUG, LOG_INFO);
+ Queue::update($hash);
+ continue;
+ }
+
+
+ \Zotlabs\Daemon\Master::Summon(array('Deliver', $hash));
+ $total = $total - 1;
+
+ if($interval && $total)
+ @time_sleep_until(microtime(true) + (float) $interval);
+ }
+ }
+
+ /**
+ * @brief
+ *
+ * @param array $sender
+ * @param array $arr
+ * @param array $deliveries
+ * @return array
+ */
+
+ static function process_channel_sync_delivery($sender, $arr, $deliveries) {
+
+ require_once('include/import.php');
+
+ $result = [];
+
+ $keychange = ((array_key_exists('keychange',$arr)) ? true : false);
+
+ foreach ($deliveries as $d) {
+ $r = q("select * from channel where channel_hash = '%s' limit 1",
+ dbesc($sender)
+ );
+
+ $DR = new \Zotlabs\Lib\DReport(z_root(),$sender,$d,'sync');
+
+ if (! $r) {
+ $DR->update('recipient not found');
+ $result[] = $DR->get();
+ continue;
+ }
+
+ $channel = $r[0];
+
+ $DR->set_name($channel['channel_name'] . ' <' . channel_reddress($channel) . '>');
+
+ $max_friends = service_class_fetch($channel['channel_id'],'total_channels');
+ $max_feeds = account_service_class_fetch($channel['channel_account_id'],'total_feeds');
+
+ if($channel['channel_hash'] != $sender) {
+ logger('Possible forgery. Sender ' . $sender . ' is not ' . $channel['channel_hash']);
+ $DR->update('channel mismatch');
+ $result[] = $DR->get();
+ continue;
+ }
+
+ if($keychange) {
+ self::keychange($channel,$arr);
+ continue;
+ }
+
+ // if the clone is active, so are we
+
+ if(substr($channel['channel_active'],0,10) !== substr(datetime_convert(),0,10)) {
+ q("UPDATE channel set channel_active = '%s' where channel_id = %d",
+ dbesc(datetime_convert()),
+ intval($channel['channel_id'])
+ );
+ }
+
+ if(array_key_exists('config',$arr) && is_array($arr['config']) && count($arr['config'])) {
+ foreach($arr['config'] as $cat => $k) {
+ foreach($arr['config'][$cat] as $k => $v)
+ set_pconfig($channel['channel_id'],$cat,$k,$v);
+ }
+ }
+
+ if(array_key_exists('obj',$arr) && $arr['obj'])
+ sync_objs($channel,$arr['obj']);
+
+ if(array_key_exists('likes',$arr) && $arr['likes'])
+ import_likes($channel,$arr['likes']);
+
+ if(array_key_exists('app',$arr) && $arr['app'])
+ sync_apps($channel,$arr['app']);
+
+ if(array_key_exists('chatroom',$arr) && $arr['chatroom'])
+ sync_chatrooms($channel,$arr['chatroom']);
+
+ if(array_key_exists('conv',$arr) && $arr['conv'])
+ import_conv($channel,$arr['conv']);
+
+ if(array_key_exists('mail',$arr) && $arr['mail'])
+ sync_mail($channel,$arr['mail']);
+
+ if(array_key_exists('event',$arr) && $arr['event'])
+ sync_events($channel,$arr['event']);
+
+ if(array_key_exists('event_item',$arr) && $arr['event_item'])
+ sync_items($channel,$arr['event_item'],((array_key_exists('relocate',$arr)) ? $arr['relocate'] : null));
+
+ if(array_key_exists('item',$arr) && $arr['item'])
+ sync_items($channel,$arr['item'],((array_key_exists('relocate',$arr)) ? $arr['relocate'] : null));
+
+ // deprecated, maintaining for a few months for upward compatibility
+ // this should sync webpages, but the logic is a bit subtle
+
+ if(array_key_exists('item_id',$arr) && $arr['item_id'])
+ sync_items($channel,$arr['item_id']);
+
+ if(array_key_exists('menu',$arr) && $arr['menu'])
+ sync_menus($channel,$arr['menu']);
+
+ if(array_key_exists('file',$arr) && $arr['file'])
+ sync_files($channel,$arr['file']);
+
+ if(array_key_exists('wiki',$arr) && $arr['wiki'])
+ sync_items($channel,$arr['wiki'],((array_key_exists('relocate',$arr)) ? $arr['relocate'] : null));
+
+ if(array_key_exists('channel',$arr) && is_array($arr['channel']) && count($arr['channel'])) {
+
+ $remote_channel = $arr['channel'];
+ $remote_channel['channel_id'] = $channel['channel_id'];
+
+ if(array_key_exists('channel_pageflags',$arr['channel']) && intval($arr['channel']['channel_pageflags'])) {
+
+ // Several pageflags are site-specific and cannot be sync'd.
+ // Only allow those bits which are shareable from the remote and then
+ // logically OR with the local flags
+
+ $arr['channel']['channel_pageflags'] = $arr['channel']['channel_pageflags'] & (PAGE_HIDDEN|PAGE_AUTOCONNECT|PAGE_APPLICATION|PAGE_PREMIUM|PAGE_ADULT);
+ $arr['channel']['channel_pageflags'] = $arr['channel']['channel_pageflags'] | $channel['channel_pageflags'];
+
+ }
+
+ $disallowed = [
+ 'channel_id', 'channel_account_id', 'channel_primary', 'channel_prvkey',
+ 'channel_address', 'channel_notifyflags', 'channel_removed', 'channel_deleted',
+ 'channel_system', 'channel_r_stream', 'channel_r_profile', 'channel_r_abook',
+ 'channel_r_storage', 'channel_r_pages', 'channel_w_stream', 'channel_w_wall',
+ 'channel_w_comment', 'channel_w_mail', 'channel_w_like', 'channel_w_tagwall',
+ 'channel_w_chat', 'channel_w_storage', 'channel_w_pages', 'channel_a_republish',
+ 'channel_a_delegate'
+ ];
+
+ $clean = array();
+ foreach($arr['channel'] as $k => $v) {
+ if(in_array($k,$disallowed))
+ continue;
+ $clean[$k] = $v;
+ }
+ if(count($clean)) {
+ foreach($clean as $k => $v) {
+ $r = dbq("UPDATE channel set " . dbesc($k) . " = '" . dbesc($v)
+ . "' where channel_id = " . intval($channel['channel_id']) );
+ }
+ }
+ }
+
+ if(array_key_exists('abook',$arr) && is_array($arr['abook']) && count($arr['abook'])) {
+ $total_friends = 0;
+ $total_feeds = 0;
+
+ $r = q("select abook_id, abook_feed from abook where abook_channel = %d",
+ intval($channel['channel_id'])
+ );
+ if($r) {
+ // don't count yourself
+ $total_friends = ((count($r) > 0) ? count($r) - 1 : 0);
+ foreach($r as $rr)
+ if(intval($rr['abook_feed']))
+ $total_feeds ++;
+ }
+
+
+ $disallowed = array('abook_id','abook_account','abook_channel','abook_rating','abook_rating_text','abook_not_here');
+
+ $fields = db_columns($abook);
+
+ foreach($arr['abook'] as $abook) {
+
+ $abconfig = null;
+
+ if(array_key_exists('abconfig',$abook) && is_array($abook['abconfig']) && count($abook['abconfig']))
+ $abconfig = $abook['abconfig'];
+
+ if(! array_key_exists('abook_blocked',$abook)) {
+ // convert from redmatrix
+ $abook['abook_blocked'] = (($abook['abook_flags'] & 0x0001) ? 1 : 0);
+ $abook['abook_ignored'] = (($abook['abook_flags'] & 0x0002) ? 1 : 0);
+ $abook['abook_hidden'] = (($abook['abook_flags'] & 0x0004) ? 1 : 0);
+ $abook['abook_archived'] = (($abook['abook_flags'] & 0x0008) ? 1 : 0);
+ $abook['abook_pending'] = (($abook['abook_flags'] & 0x0010) ? 1 : 0);
+ $abook['abook_unconnected'] = (($abook['abook_flags'] & 0x0020) ? 1 : 0);
+ $abook['abook_self'] = (($abook['abook_flags'] & 0x0080) ? 1 : 0);
+ $abook['abook_feed'] = (($abook['abook_flags'] & 0x0100) ? 1 : 0);
+ }
+
+ $clean = array();
+ if($abook['abook_xchan'] && $abook['entry_deleted']) {
+ logger('Removing abook entry for ' . $abook['abook_xchan']);
+
+ $r = q("select abook_id, abook_feed from abook where abook_xchan = '%s' and abook_channel = %d and abook_self = 0 limit 1",
+ dbesc($abook['abook_xchan']),
+ intval($channel['channel_id'])
+ );
+ if($r) {
+ contact_remove($channel['channel_id'],$r[0]['abook_id']);
+ if($total_friends)
+ $total_friends --;
+ if(intval($r[0]['abook_feed']))
+ $total_feeds --;
+ }
+ continue;
+ }
+
+ // Perform discovery if the referenced xchan hasn't ever been seen on this hub.
+ // This relies on the undocumented behaviour that red sites send xchan info with the abook
+ // and import_author_xchan will look them up on all federated networks
+
+ if($abook['abook_xchan'] && $abook['xchan_addr']) {
+ $h = Libzot::get_hublocs($abook['abook_xchan']);
+ if(! $h) {
+ $xhash = import_author_xchan(encode_item_xchan($abook));
+ if(! $xhash) {
+ logger('Import of ' . $abook['xchan_addr'] . ' failed.');
+ continue;
+ }
+ }
+ }
+
+ foreach($abook as $k => $v) {
+ if(in_array($k,$disallowed) || (strpos($k,'abook') !== 0)) {
+ continue;
+ }
+ if(! in_array($k,$fields)) {
+ continue;
+ }
+ $clean[$k] = $v;
+ }
+
+ if(! array_key_exists('abook_xchan',$clean))
+ continue;
+
+ if(array_key_exists('abook_instance',$clean) && $clean['abook_instance'] && strpos($clean['abook_instance'],z_root()) === false) {
+ $clean['abook_not_here'] = 1;
+ }
+
+
+ $r = q("select * from abook where abook_xchan = '%s' and abook_channel = %d limit 1",
+ dbesc($clean['abook_xchan']),
+ intval($channel['channel_id'])
+ );
+
+ // make sure we have an abook entry for this xchan on this system
+
+ if(! $r) {
+ if($max_friends !== false && $total_friends > $max_friends) {
+ logger('total_channels service class limit exceeded');
+ continue;
+ }
+ if($max_feeds !== false && intval($clean['abook_feed']) && $total_feeds > $max_feeds) {
+ logger('total_feeds service class limit exceeded');
+ continue;
+ }
+ abook_store_lowlevel(
+ [
+ 'abook_xchan' => $clean['abook_xchan'],
+ 'abook_account' => $channel['channel_account_id'],
+ 'abook_channel' => $channel['channel_id']
+ ]
+ );
+ $total_friends ++;
+ if(intval($clean['abook_feed']))
+ $total_feeds ++;
+ }
+
+ if(count($clean)) {
+ foreach($clean as $k => $v) {
+ if($k == 'abook_dob')
+ $v = dbescdate($v);
+
+ $r = dbq("UPDATE abook set " . dbesc($k) . " = '" . dbesc($v)
+ . "' where abook_xchan = '" . dbesc($clean['abook_xchan']) . "' and abook_channel = " . intval($channel['channel_id']));
+ }
+ }
+
+ // This will set abconfig vars if the sender is using old-style fixed permissions
+ // using the raw abook record as passed to us. New-style permissions will fall through
+ // and be set using abconfig
+
+ // translate_abook_perms_inbound($channel,$abook);
+
+ if($abconfig) {
+ /// @fixme does not handle sync of del_abconfig
+ foreach($abconfig as $abc) {
+ set_abconfig($channel['channel_id'],$abc['xchan'],$abc['cat'],$abc['k'],$abc['v']);
+ }
+ }
+ }
+ }
+
+ // sync collections (privacy groups) oh joy...
+
+ if(array_key_exists('collections',$arr) && is_array($arr['collections']) && count($arr['collections'])) {
+ $x = q("select * from pgrp where uid = %d",
+ intval($channel['channel_id'])
+ );
+ foreach($arr['collections'] as $cl) {
+ $found = false;
+ if($x) {
+ foreach($x as $y) {
+ if($cl['collection'] == $y['hash']) {
+ $found = true;
+ break;
+ }
+ }
+ if($found) {
+ if(($y['gname'] != $cl['name'])
+ || ($y['visible'] != $cl['visible'])
+ || ($y['deleted'] != $cl['deleted'])) {
+ q("update pgrp set gname = '%s', visible = %d, deleted = %d where hash = '%s' and uid = %d",
+ dbesc($cl['name']),
+ intval($cl['visible']),
+ intval($cl['deleted']),
+ dbesc($cl['collection']),
+ intval($channel['channel_id'])
+ );
+ }
+ if(intval($cl['deleted']) && (! intval($y['deleted']))) {
+ q("delete from pgrp_member where gid = %d",
+ intval($y['id'])
+ );
+ }
+ }
+ }
+ if(! $found) {
+ $r = q("INSERT INTO pgrp ( hash, uid, visible, deleted, gname )
+ VALUES( '%s', %d, %d, %d, '%s' ) ",
+ dbesc($cl['collection']),
+ intval($channel['channel_id']),
+ intval($cl['visible']),
+ intval($cl['deleted']),
+ dbesc($cl['name'])
+ );
+ }
+
+ // now look for any collections locally which weren't in the list we just received.
+ // They need to be removed by marking deleted and removing the members.
+ // This shouldn't happen except for clones created before this function was written.
+
+ if($x) {
+ $found_local = false;
+ foreach($x as $y) {
+ foreach($arr['collections'] as $cl) {
+ if($cl['collection'] == $y['hash']) {
+ $found_local = true;
+ break;
+ }
+ }
+ if(! $found_local) {
+ q("delete from pgrp_member where gid = %d",
+ intval($y['id'])
+ );
+ q("update pgrp set deleted = 1 where id = %d and uid = %d",
+ intval($y['id']),
+ intval($channel['channel_id'])
+ );
+ }
+ }
+ }
+ }
+
+ // reload the group list with any updates
+ $x = q("select * from pgrp where uid = %d",
+ intval($channel['channel_id'])
+ );
+
+ // now sync the members
+
+ if(array_key_exists('collection_members', $arr)
+ && is_array($arr['collection_members'])
+ && count($arr['collection_members'])) {
+
+ // first sort into groups keyed by the group hash
+ $members = array();
+ foreach($arr['collection_members'] as $cm) {
+ if(! array_key_exists($cm['collection'],$members))
+ $members[$cm['collection']] = array();
+
+ $members[$cm['collection']][] = $cm['member'];
+ }
+
+ // our group list is already synchronised
+ if($x) {
+ foreach($x as $y) {
+
+ // for each group, loop on members list we just received
+ if(isset($y['hash']) && isset($members[$y['hash']])) {
+ foreach($members[$y['hash']] as $member) {
+ $found = false;
+ $z = q("select xchan from pgrp_member where gid = %d and uid = %d and xchan = '%s' limit 1",
+ intval($y['id']),
+ intval($channel['channel_id']),
+ dbesc($member)
+ );
+ if($z)
+ $found = true;
+
+ // if somebody is in the group that wasn't before - add them
+
+ if(! $found) {
+ q("INSERT INTO pgrp_member (uid, gid, xchan)
+ VALUES( %d, %d, '%s' ) ",
+ intval($channel['channel_id']),
+ intval($y['id']),
+ dbesc($member)
+ );
+ }
+ }
+ }
+
+ // now retrieve a list of members we have on this site
+ $m = q("select xchan from pgrp_member where gid = %d and uid = %d",
+ intval($y['id']),
+ intval($channel['channel_id'])
+ );
+ if($m) {
+ foreach($m as $mm) {
+ // if the local existing member isn't in the list we just received - remove them
+ if(! in_array($mm['xchan'],$members[$y['hash']])) {
+ q("delete from pgrp_member where xchan = '%s' and gid = %d and uid = %d",
+ dbesc($mm['xchan']),
+ intval($y['id']),
+ intval($channel['channel_id'])
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if(array_key_exists('profile',$arr) && is_array($arr['profile']) && count($arr['profile'])) {
+
+ $disallowed = array('id','aid','uid','guid');
+
+ foreach($arr['profile'] as $profile) {
+
+ $x = q("select * from profile where profile_guid = '%s' and uid = %d limit 1",
+ dbesc($profile['profile_guid']),
+ intval($channel['channel_id'])
+ );
+ if(! $x) {
+ profile_store_lowlevel(
+ [
+ 'aid' => $channel['channel_account_id'],
+ 'uid' => $channel['channel_id'],
+ 'profile_guid' => $profile['profile_guid'],
+ ]
+ );
+
+ $x = q("select * from profile where profile_guid = '%s' and uid = %d limit 1",
+ dbesc($profile['profile_guid']),
+ intval($channel['channel_id'])
+ );
+ if(! $x)
+ continue;
+ }
+ $clean = array();
+ foreach($profile as $k => $v) {
+ if(in_array($k,$disallowed))
+ continue;
+
+ if($profile['is_default'] && in_array($k,['photo','thumb']))
+ continue;
+
+ if($k === 'name')
+ $clean['fullname'] = $v;
+ elseif($k === 'with')
+ $clean['partner'] = $v;
+ elseif($k === 'work')
+ $clean['employment'] = $v;
+ elseif(array_key_exists($k,$x[0]))
+ $clean[$k] = $v;
+
+ /**
+ * @TODO
+ * We also need to import local photos if a custom photo is selected
+ */
+
+ if((strpos($profile['thumb'],'/photo/profile/l/') !== false) || intval($profile['is_default'])) {
+ $profile['photo'] = z_root() . '/photo/profile/l/' . $channel['channel_id'];
+ $profile['thumb'] = z_root() . '/photo/profile/m/' . $channel['channel_id'];
+ }
+ else {
+ $profile['photo'] = z_root() . '/photo/' . basename($profile['photo']);
+ $profile['thumb'] = z_root() . '/photo/' . basename($profile['thumb']);
+ }
+ }
+
+ if(count($clean)) {
+ foreach($clean as $k => $v) {
+ $r = dbq("UPDATE profile set " . TQUOT . dbesc($k) . TQUOT . " = '" . dbesc($v)
+ . "' where profile_guid = '" . dbesc($profile['profile_guid'])
+ . "' and uid = " . intval($channel['channel_id']));
+ }
+ }
+ }
+ }
+
+ $addon = ['channel' => $channel, 'data' => $arr];
+ /**
+ * @hooks process_channel_sync_delivery
+ * Called when accepting delivery of a 'sync packet' containing structure and table updates from a channel clone.
+ * * \e array \b channel
+ * * \e array \b data
+ */
+ call_hooks('process_channel_sync_delivery', $addon);
+
+ $DR = new \Zotlabs\Lib\DReport(z_root(),$d,$d,'sync','channel sync delivered');
+
+ $DR->set_name($channel['channel_name'] . ' <' . channel_reddress($channel) . '>');
+
+ $result[] = $DR->get();
+ }
+
+ return $result;
+ }
+
+ /**
+ * @brief Synchronises locations.
+ *
+ * @param array $sender
+ * @param array $arr
+ * @param boolean $absolute (optional) default false
+ * @return array
+ */
+
+ static function sync_locations($sender, $arr, $absolute = false) {
+
+ $ret = array();
+
+ if($arr['locations']) {
+
+ if($absolute)
+ self::check_location_move($sender['hash'],$arr['locations']);
+
+ $xisting = q("select * from hubloc where hubloc_hash = '%s'",
+ dbesc($sender['hash'])
+ );
+
+ // See if a primary is specified
+
+ $has_primary = false;
+ foreach($arr['locations'] as $location) {
+ if($location['primary']) {
+ $has_primary = true;
+ break;
+ }
+ }
+
+ // Ensure that they have one primary hub
+
+ if(! $has_primary)
+ $arr['locations'][0]['primary'] = true;
+
+ foreach($arr['locations'] as $location) {
+ if(! Libzot::verify($location['url'],$location['url_sig'],$sender['public_key'])) {
+ logger('Unable to verify site signature for ' . $location['url']);
+ $ret['message'] .= sprintf( t('Unable to verify site signature for %s'), $location['url']) . EOL;
+ continue;
+ }
+
+ for($x = 0; $x < count($xisting); $x ++) {
+ if(($xisting[$x]['hubloc_url'] === $location['url'])
+ && ($xisting[$x]['hubloc_sitekey'] === $location['sitekey'])) {
+ $xisting[$x]['updated'] = true;
+ }
+ }
+
+ if(! $location['sitekey']) {
+ logger('Empty hubloc sitekey. ' . print_r($location,true));
+ continue;
+ }
+
+ // Catch some malformed entries from the past which still exist
+
+ if(strpos($location['address'],'/') !== false)
+ $location['address'] = substr($location['address'],0,strpos($location['address'],'/'));
+
+ // match as many fields as possible in case anything at all changed.
+
+ $r = q("select * from hubloc where hubloc_hash = '%s' and hubloc_guid = '%s' and hubloc_guid_sig = '%s' and hubloc_id_url = '%s' and hubloc_url = '%s' and hubloc_url_sig = '%s' and hubloc_site_id = '%s' and hubloc_host = '%s' and hubloc_addr = '%s' and hubloc_callback = '%s' and hubloc_sitekey = '%s' ",
+ dbesc($sender['hash']),
+ dbesc($sender['id']),
+ dbesc($sender['id_sig']),
+ dbesc($location['id_url']),
+ dbesc($location['url']),
+ dbesc($location['url_sig']),
+ dbesc($location['site_id']),
+ dbesc($location['host']),
+ dbesc($location['address']),
+ dbesc($location['callback']),
+ dbesc($location['sitekey'])
+ );
+ if($r) {
+ logger('Hub exists: ' . $location['url'], LOGGER_DEBUG);
+
+ // update connection timestamp if this is the site we're talking to
+ // This only happens when called from import_xchan
+
+ $current_site = false;
+
+ $t = datetime_convert('UTC','UTC','now - 15 minutes');
+
+ if(array_key_exists('site',$arr) && $location['url'] == $arr['site']['url']) {
+ q("update hubloc set hubloc_connected = '%s', hubloc_updated = '%s' where hubloc_id = %d and hubloc_connected < '%s'",
+ dbesc(datetime_convert()),
+ dbesc(datetime_convert()),
+ intval($r[0]['hubloc_id']),
+ dbesc($t)
+ );
+ $current_site = true;
+ }
+
+ if($current_site && intval($r[0]['hubloc_error'])) {
+ q("update hubloc set hubloc_error = 0 where hubloc_id = %d",
+ intval($r[0]['hubloc_id'])
+ );
+ if(intval($r[0]['hubloc_orphancheck'])) {
+ q("update hubloc set hubloc_orphancheck = 0 where hubloc_id = %d",
+ intval($r[0]['hubloc_id'])
+ );
+ }
+ q("update xchan set xchan_orphan = 0 where xchan_orphan = 1 and xchan_hash = '%s'",
+ dbesc($sender['hash'])
+ );
+ }
+
+ // Remove pure duplicates
+ if(count($r) > 1) {
+ for($h = 1; $h < count($r); $h ++) {
+ q("delete from hubloc where hubloc_id = %d",
+ intval($r[$h]['hubloc_id'])
+ );
+ $what .= 'duplicate_hubloc_removed ';
+ $changed = true;
+ }
+ }
+
+ if(intval($r[0]['hubloc_primary']) && (! $location['primary'])) {
+ $m = q("update hubloc set hubloc_primary = 0, hubloc_updated = '%s' where hubloc_id = %d",
+ dbesc(datetime_convert()),
+ intval($r[0]['hubloc_id'])
+ );
+ $r[0]['hubloc_primary'] = intval($location['primary']);
+ hubloc_change_primary($r[0]);
+ $what .= 'primary_hub ';
+ $changed = true;
+ }
+ elseif((! intval($r[0]['hubloc_primary'])) && ($location['primary'])) {
+ $m = q("update hubloc set hubloc_primary = 1, hubloc_updated = '%s' where hubloc_id = %d",
+ dbesc(datetime_convert()),
+ intval($r[0]['hubloc_id'])
+ );
+ // make sure hubloc_change_primary() has current data
+ $r[0]['hubloc_primary'] = intval($location['primary']);
+ hubloc_change_primary($r[0]);
+ $what .= 'primary_hub ';
+ $changed = true;
+ }
+ elseif($absolute) {
+ // Absolute sync - make sure the current primary is correctly reflected in the xchan
+ $pr = hubloc_change_primary($r[0]);
+ if($pr) {
+ $what .= 'xchan_primary ';
+ $changed = true;
+ }
+ }
+ if(intval($r[0]['hubloc_deleted']) && (! intval($location['deleted']))) {
+ $n = q("update hubloc set hubloc_deleted = 0, hubloc_updated = '%s' where hubloc_id = %d",
+ dbesc(datetime_convert()),
+ intval($r[0]['hubloc_id'])
+ );
+ $what .= 'undelete_hub ';
+ $changed = true;
+ }
+ elseif((! intval($r[0]['hubloc_deleted'])) && (intval($location['deleted']))) {
+ logger('deleting hubloc: ' . $r[0]['hubloc_addr']);
+ $n = q("update hubloc set hubloc_deleted = 1, hubloc_updated = '%s' where hubloc_id = %d",
+ dbesc(datetime_convert()),
+ intval($r[0]['hubloc_id'])
+ );
+ $what .= 'delete_hub ';
+ $changed = true;
+ }
+ continue;
+ }
+
+ // Existing hubs are dealt with. Now let's process any new ones.
+ // New hub claiming to be primary. Make it so by removing any existing primaries.
+
+ if(intval($location['primary'])) {
+ $r = q("update hubloc set hubloc_primary = 0, hubloc_updated = '%s' where hubloc_hash = '%s' and hubloc_primary = 1",
+ dbesc(datetime_convert()),
+ dbesc($sender['hash'])
+ );
+ }
+
+ logger('New hub: ' . $location['url']);
+
+ $r = hubloc_store_lowlevel(
+ [
+ 'hubloc_guid' => $sender['id'],
+ 'hubloc_guid_sig' => $sender['id_sig'],
+ 'hubloc_id_url' => $location['id_url'],
+ 'hubloc_hash' => $sender['hash'],
+ 'hubloc_addr' => $location['address'],
+ 'hubloc_network' => 'zot6',
+ 'hubloc_primary' => intval($location['primary']),
+ 'hubloc_url' => $location['url'],
+ 'hubloc_url_sig' => $location['url_sig'],
+ 'hubloc_site_id' => Libzot::make_xchan_hash($location['url'],$location['sitekey']),
+ 'hubloc_host' => $location['host'],
+ 'hubloc_callback' => $location['callback'],
+ 'hubloc_sitekey' => $location['sitekey'],
+ 'hubloc_updated' => datetime_convert(),
+ 'hubloc_connected' => datetime_convert()
+ ]
+ );
+
+ $what .= 'newhub ';
+ $changed = true;
+
+ if($location['primary']) {
+ $r = q("select * from hubloc where hubloc_addr = '%s' and hubloc_sitekey = '%s' limit 1",
+ dbesc($location['address']),
+ dbesc($location['sitekey'])
+ );
+ if($r)
+ hubloc_change_primary($r[0]);
+ }
+ }
+
+ // get rid of any hubs we have for this channel which weren't reported.
+
+ if($absolute && $xisting) {
+ foreach($xisting as $x) {
+ if(! array_key_exists('updated',$x)) {
+ logger('Deleting unreferenced hub location ' . $x['hubloc_addr']);
+ $r = q("update hubloc set hubloc_deleted = 1, hubloc_updated = '%s' where hubloc_id = %d",
+ dbesc(datetime_convert()),
+ intval($x['hubloc_id'])
+ );
+ $what .= 'removed_hub ';
+ $changed = true;
+ }
+ }
+ }
+ }
+ else {
+ logger('No locations to sync!');
+ }
+
+ $ret['change_message'] = $what;
+ $ret['changed'] = $changed;
+
+ return $ret;
+ }
+
+
+ static function keychange($channel,$arr) {
+
+ // verify the keychange operation
+ if(! Libzot::verify($arr['channel']['channel_pubkey'],$arr['keychange']['new_sig'],$channel['channel_prvkey'])) {
+ logger('sync keychange: verification failed');
+ return;
+ }
+
+ $sig = Libzot::sign($channel['channel_guid'],$arr['channel']['channel_prvkey']);
+ $hash = Libzot::make_xchan_hash($channel['channel_guid'],$arr['channel']['channel_pubkey']);
+
+
+ $r = q("update channel set channel_prvkey = '%s', channel_pubkey = '%s', channel_guid_sig = '%s',
+ channel_hash = '%s' where channel_id = %d",
+ dbesc($arr['channel']['channel_prvkey']),
+ dbesc($arr['channel']['channel_pubkey']),
+ dbesc($sig),
+ dbesc($hash),
+ intval($channel['channel_id'])
+ );
+ if(! $r) {
+ logger('keychange sync: channel update failed');
+ return;
+ }
+
+ $r = q("select * from channel where channel_id = %d",
+ intval($channel['channel_id'])
+ );
+
+ if(! $r) {
+ logger('keychange sync: channel retrieve failed');
+ return;
+ }
+
+ $channel = $r[0];
+
+ $h = q("select * from hubloc where hubloc_hash = '%s' and hubloc_url = '%s' ",
+ dbesc($arr['keychange']['old_hash']),
+ dbesc(z_root())
+ );
+
+ if($h) {
+ foreach($h as $hv) {
+ $hv['hubloc_guid_sig'] = $sig;
+ $hv['hubloc_hash'] = $hash;
+ $hv['hubloc_url_sig'] = Libzot::sign(z_root(),$channel['channel_prvkey']);
+ hubloc_store_lowlevel($hv);
+ }
+ }
+
+ $x = q("select * from xchan where xchan_hash = '%s' ",
+ dbesc($arr['keychange']['old_hash'])
+ );
+
+ $check = q("select * from xchan where xchan_hash = '%s'",
+ dbesc($hash)
+ );
+
+ if(($x) && (! $check)) {
+ $oldxchan = $x[0];
+ foreach($x as $xv) {
+ $xv['xchan_guid_sig'] = $sig;
+ $xv['xchan_hash'] = $hash;
+ $xv['xchan_pubkey'] = $channel['channel_pubkey'];
+ xchan_store_lowlevel($xv);
+ $newxchan = $xv;
+ }
+ }
+
+ $a = q("select * from abook where abook_xchan = '%s' and abook_self = 1",
+ dbesc($arr['keychange']['old_hash'])
+ );
+
+ if($a) {
+ q("update abook set abook_xchan = '%s' where abook_id = %d",
+ dbesc($hash),
+ intval($a[0]['abook_id'])
+ );
+ }
+
+ xchan_change_key($oldxchan,$newxchan,$arr['keychange']);
+
+ }
+
+}
\ No newline at end of file
diff --git a/Zotlabs/Lib/Libzot.php b/Zotlabs/Lib/Libzot.php
new file mode 100644
index 000000000..ec9db4ce1
--- /dev/null
+++ b/Zotlabs/Lib/Libzot.php
@@ -0,0 +1,2849 @@
+ $type,
+ 'encoding' => $encoding,
+ 'sender' => $channel['channel_hash'],
+ 'site_id' => self::make_xchan_hash(z_root(), get_config('system','pubkey')),
+ 'version' => System::get_zot_revision(),
+ ];
+
+ if ($recipients) {
+ $data['recipients'] = $recipients;
+ }
+
+ if ($msg) {
+ $actor = channel_url($channel);
+ if ($encoding === 'activitystreams' && array_key_exists('actor',$msg) && is_string($msg['actor']) && $actor === $msg['actor']) {
+ $msg = JSalmon::sign($msg,$actor,$channel['channel_prvkey']);
+ }
+ $data['data'] = $msg;
+ }
+ else {
+ unset($data['encoding']);
+ }
+
+ logger('packet: ' . print_r($data,true), LOGGER_DATA, LOG_DEBUG);
+
+ if ($remote_key) {
+ $algorithm = self::best_algorithm($methods);
+ if ($algorithm) {
+ $data = crypto_encapsulate(json_encode($data),$remote_key, $algorithm);
+ }
+ }
+
+ return json_encode($data);
+ }
+
+
+ /**
+ * @brief Choose best encryption function from those available on both sites.
+ *
+ * @param string $methods
+ * comma separated list of encryption methods
+ * @return string first match from our site method preferences crypto_methods() array
+ * of a method which is common to both sites; or 'aes256cbc' if no matches are found.
+ */
+
+ static function best_algorithm($methods) {
+
+ $x = [
+ 'methods' => $methods,
+ 'result' => ''
+ ];
+
+ /**
+ * @hooks zot_best_algorithm
+ * Called when negotiating crypto algorithms with remote sites.
+ * * \e string \b methods - comma separated list of encryption methods
+ * * \e string \b result - the algorithm to return
+ */
+
+ call_hooks('zot_best_algorithm', $x);
+
+ if($x['result'])
+ return $x['result'];
+
+ if($methods) {
+ $x = explode(',', $methods);
+ if($x) {
+ $y = crypto_methods();
+ if($y) {
+ foreach($y as $yv) {
+ $yv = trim($yv);
+ if(in_array($yv, $x)) {
+ return($yv);
+ }
+ }
+ }
+ }
+ }
+
+ return '';
+ }
+
+
+ /**
+ * @brief send a zot message
+ *
+ * @see z_post_url()
+ *
+ * @param string $url
+ * @param array $data
+ * @param array $channel (required if using zot6 delivery)
+ * @param array $crypto (required if encrypted httpsig, requires hubloc_sitekey and site_crypto elements)
+ * @return array see z_post_url() for returned data format
+ */
+
+ static function zot($url, $data, $channel = null,$crypto = null) {
+
+ if($channel) {
+ $headers = [
+ 'X-Zot-Token' => random_string(),
+ 'Digest' => HTTPSig::generate_digest_header($data),
+ 'Content-type' => 'application/x-zot+json'
+ ];
+
+ $h = HTTPSig::create_sig($headers,$channel['channel_prvkey'],channel_url($channel),false,'sha512',
+ (($crypto) ? [ 'key' => $crypto['hubloc_sitekey'], 'algorithm' => self::best_algorithm($crypto['site_crypto']) ] : false));
+ }
+ else {
+ $h = [];
+ }
+
+ $redirects = 0;
+
+ return z_post_url($url,$data,$redirects,((empty($h)) ? [] : [ 'headers' => $h ]));
+ }
+
+
+ /**
+ * @brief Refreshes after permission changed or friending, etc.
+ *
+ *
+ * refresh is typically invoked when somebody has changed permissions of a channel and they are notified
+ * to fetch new permissions via a finger/discovery operation. This may result in a new connection
+ * (abook entry) being added to a local channel and it may result in auto-permissions being granted.
+ *
+ * Friending in zot is accomplished by sending a refresh packet to a specific channel which indicates a
+ * permission change has been made by the sender which affects the target channel. The hub controlling
+ * the target channel does targetted discovery (a zot-finger request requesting permissions for the local
+ * channel). These are decoded here, and if necessary and abook structure (addressbook) is created to store
+ * the permissions assigned to this channel.
+ *
+ * Initially these abook structures are created with a 'pending' flag, so that no reverse permissions are
+ * implied until this is approved by the owner channel. A channel can also auto-populate permissions in
+ * return and send back a refresh packet of its own. This is used by forum and group communication channels
+ * so that friending and membership in the channel's "club" is automatic.
+ *
+ * @param array $them => xchan structure of sender
+ * @param array $channel => local channel structure of target recipient, required for "friending" operations
+ * @param array $force (optional) default false
+ *
+ * @return boolean
+ * * \b true if successful
+ * * otherwise \b false
+ */
+
+ static function refresh($them, $channel = null, $force = false) {
+
+ logger('them: ' . print_r($them,true), LOGGER_DATA, LOG_DEBUG);
+ if ($channel)
+ logger('channel: ' . print_r($channel,true), LOGGER_DATA, LOG_DEBUG);
+
+ $url = null;
+
+ if ($them['hubloc_id_url']) {
+ $url = $them['hubloc_id_url'];
+ }
+ else {
+ $r = null;
+
+ // if they re-installed the server we could end up with the wrong record - pointing to the old install.
+ // We'll order by reverse id to try and pick off the newest one first and hopefully end up with the
+ // correct hubloc. If this doesn't work we may have to re-write this section to try them all.
+
+ if(array_key_exists('xchan_addr',$them) && $them['xchan_addr']) {
+ $r = q("select hubloc_id_url, hubloc_primary from hubloc where hubloc_addr = '%s' order by hubloc_id desc",
+ dbesc($them['xchan_addr'])
+ );
+ }
+ if(! $r) {
+ $r = q("select hubloc_id_url, hubloc_primary from hubloc where hubloc_hash = '%s' order by hubloc_id desc",
+ dbesc($them['xchan_hash'])
+ );
+ }
+
+ if ($r) {
+ foreach ($r as $rr) {
+ if (intval($rr['hubloc_primary'])) {
+ $url = $rr['hubloc_id_url'];
+ $record = $rr;
+ }
+ }
+ if (! $url) {
+ $url = $r[0]['hubloc_id_url'];
+ }
+ }
+ }
+ if (! $url) {
+ logger('zot_refresh: no url');
+ return false;
+ }
+
+ $s = q("select site_dead from site where site_url = '%s' limit 1",
+ dbesc($url)
+ );
+
+ if($s && intval($s[0]['site_dead']) && (! $force)) {
+ logger('zot_refresh: site ' . $url . ' is marked dead and force flag is not set. Cancelling operation.');
+ return false;
+ }
+
+ $record = Zotfinger::exec($url,$channel);
+
+ // Check the HTTP signature
+
+ $hsig = $record['signature'];
+ if($hsig && $hsig['signer'] === $url && $hsig['header_valid'] === true && $hsig['content_valid'] === true)
+ $hsig_valid = true;
+
+ if(! $hsig_valid) {
+ logger('http signature not valid: ' . print_r($hsig,true));
+ return $result;
+ }
+
+
+ logger('zot-info: ' . print_r($record,true), LOGGER_DATA, LOG_DEBUG);
+
+ $x = self::import_xchan($record['data'], (($force) ? UPDATE_FLAGS_FORCED : UPDATE_FLAGS_UPDATED));
+
+ if(! $x['success'])
+ return false;
+
+ if($channel && $record['data']['permissions']) {
+ $old_read_stream_perm = their_perms_contains($channel['channel_id'],$x['hash'],'view_stream');
+ set_abconfig($channel['channel_id'],$x['hash'],'system','their_perms',$record['data']['permissions']);
+
+ if(array_key_exists('profile',$record['data']) && array_key_exists('next_birthday',$record['data']['profile'])) {
+ $next_birthday = datetime_convert('UTC','UTC',$record['data']['profile']['next_birthday']);
+ }
+ else {
+ $next_birthday = NULL_DATE;
+ }
+
+ $profile_assign = get_pconfig($channel['channel_id'],'system','profile_assign','');
+
+ // Keep original perms to check if we need to notify them
+ $previous_perms = get_all_perms($channel['channel_id'],$x['hash']);
+
+ $r = q("select * from abook where abook_xchan = '%s' and abook_channel = %d and abook_self = 0 limit 1",
+ dbesc($x['hash']),
+ intval($channel['channel_id'])
+ );
+
+ if($r) {
+
+ // connection exists
+
+ // if the dob is the same as what we have stored (disregarding the year), keep the one
+ // we have as we may have updated the year after sending a notification; and resetting
+ // to the one we just received would cause us to create duplicated events.
+
+ if(substr($r[0]['abook_dob'],5) == substr($next_birthday,5))
+ $next_birthday = $r[0]['abook_dob'];
+
+ $y = q("update abook set abook_dob = '%s'
+ where abook_xchan = '%s' and abook_channel = %d
+ and abook_self = 0 ",
+ dbescdate($next_birthday),
+ dbesc($x['hash']),
+ intval($channel['channel_id'])
+ );
+
+ if(! $y)
+ logger('abook update failed');
+ else {
+ // if we were just granted read stream permission and didn't have it before, try to pull in some posts
+ if((! $old_read_stream_perm) && (intval($permissions['view_stream'])))
+ \Zotlabs\Daemon\Master::Summon(array('Onepoll',$r[0]['abook_id']));
+ }
+ }
+ else {
+
+ $p = \Zotlabs\Access\Permissions::connect_perms($channel['channel_id']);
+ $my_perms = \Zotlabs\Access\Permissions::serialise($p['perms']);
+
+ $automatic = $p['automatic'];
+
+ // new connection
+
+ if($my_perms) {
+ set_abconfig($channel['channel_id'],$x['hash'],'system','my_perms',$my_perms);
+ }
+
+ $closeness = get_pconfig($channel['channel_id'],'system','new_abook_closeness');
+ if($closeness === false)
+ $closeness = 80;
+
+ $y = abook_store_lowlevel(
+ [
+ 'abook_account' => intval($channel['channel_account_id']),
+ 'abook_channel' => intval($channel['channel_id']),
+ 'abook_closeness' => intval($closeness),
+ 'abook_xchan' => $x['hash'],
+ 'abook_profile' => $profile_assign,
+ 'abook_created' => datetime_convert(),
+ 'abook_updated' => datetime_convert(),
+ 'abook_dob' => $next_birthday,
+ 'abook_pending' => intval(($automatic) ? 0 : 1)
+ ]
+ );
+
+ if($y) {
+ logger("New introduction received for {$channel['channel_name']}");
+ $new_perms = get_all_perms($channel['channel_id'],$x['hash']);
+
+ // Send a clone sync packet and a permissions update if permissions have changed
+
+ $new_connection = q("select * from abook left join xchan on abook_xchan = xchan_hash where abook_xchan = '%s' and abook_channel = %d and abook_self = 0 order by abook_created desc limit 1",
+ dbesc($x['hash']),
+ intval($channel['channel_id'])
+ );
+
+ if($new_connection) {
+ if(! \Zotlabs\Access\Permissions::PermsCompare($new_perms,$previous_perms))
+ \Zotlabs\Daemon\Master::Summon(array('Notifier','permissions_create',$new_connection[0]['abook_id']));
+ Enotify::submit(
+ [
+ 'type' => NOTIFY_INTRO,
+ 'from_xchan' => $x['hash'],
+ 'to_xchan' => $channel['channel_hash'],
+ 'link' => z_root() . '/connedit/' . $new_connection[0]['abook_id']
+ ]
+ );
+
+ if(intval($permissions['view_stream'])) {
+ if(intval(get_pconfig($channel['channel_id'],'perm_limits','send_stream') & PERMS_PENDING)
+ || (! intval($new_connection[0]['abook_pending'])))
+ \Zotlabs\Daemon\Master::Summon(array('Onepoll',$new_connection[0]['abook_id']));
+ }
+
+
+ // If there is a default group for this channel, add this connection to it
+ // for pending connections this will happens at acceptance time.
+
+ if(! intval($new_connection[0]['abook_pending'])) {
+ $default_group = $channel['channel_default_group'];
+ if($default_group) {
+ $g = Group::rec_byhash($channel['channel_id'],$default_group);
+ if($g)
+ Group::member_add($channel['channel_id'],'',$x['hash'],$g['id']);
+ }
+ }
+
+ unset($new_connection[0]['abook_id']);
+ unset($new_connection[0]['abook_account']);
+ unset($new_connection[0]['abook_channel']);
+ $abconfig = load_abconfig($channel['channel_id'],$new_connection['abook_xchan']);
+ if($abconfig)
+ $new_connection['abconfig'] = $abconfig;
+
+ Libsync::build_sync_packet($channel['channel_id'], array('abook' => $new_connection));
+ }
+ }
+
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @brief Look up if channel is known and previously verified.
+ *
+ * A guid and a url, both signed by the sender, distinguish a known sender at a
+ * known location.
+ * This function looks these up to see if the channel is known and therefore
+ * previously verified. If not, we will need to verify it.
+ *
+ * @param array $arr an associative array which must contain:
+ * * \e string \b id => id of conversant
+ * * \e string \b id_sig => id signed with conversant's private key
+ * * \e string \b location => URL of the origination hub of this communication
+ * * \e string \b location_sig => URL signed with conversant's private key
+ * @param boolean $multiple (optional) default false
+ *
+ * @return array|null
+ * * null if site is blacklisted or not found
+ * * otherwise an array with an hubloc record
+ */
+
+ static function gethub($arr, $multiple = false) {
+
+ if($arr['id'] && $arr['id_sig'] && $arr['location'] && $arr['location_sig']) {
+
+ if(! check_siteallowed($arr['location'])) {
+ logger('blacklisted site: ' . $arr['location']);
+ return null;
+ }
+
+ $limit = (($multiple) ? '' : ' limit 1 ');
+
+ $r = q("select hubloc.*, site.site_crypto from hubloc left join site on hubloc_url = site_url
+ where hubloc_guid = '%s' and hubloc_guid_sig = '%s'
+ and hubloc_url = '%s' and hubloc_url_sig = '%s'
+ and hubloc_site_id = '%s' $limit",
+ dbesc($arr['id']),
+ dbesc($arr['id_sig']),
+ dbesc($arr['location']),
+ dbesc($arr['location_sig']),
+ dbesc($arr['site_id'])
+ );
+ if($r) {
+ logger('Found', LOGGER_DEBUG);
+ return (($multiple) ? $r : $r[0]);
+ }
+ }
+ logger('Not found: ' . print_r($arr,true), LOGGER_DEBUG);
+
+ return false;
+ }
+
+
+
+
+ static function valid_hub($sender,$site_id) {
+
+ $r = q("select hubloc.*, site.site_crypto from hubloc left join site on hubloc_url = site_url where hubloc_hash = '%s' and hubloc_site_id = '%s' limit 1",
+ dbesc($sender),
+ dbesc($site_id)
+ );
+ if(! $r) {
+ return null;
+ }
+
+ if(! check_siteallowed($r[0]['hubloc_url'])) {
+ logger('blacklisted site: ' . $r[0]['hubloc_url']);
+ return null;
+ }
+
+ if(! check_channelallowed($r[0]['hubloc_hash'])) {
+ logger('blacklisted channel: ' . $r[0]['hubloc_hash']);
+ return null;
+ }
+
+ return $r[0];
+
+ }
+
+ /**
+ * @brief Registers an unknown hub.
+ *
+ * A communication has been received which has an unknown (to us) sender.
+ * Perform discovery based on our calculated hash of the sender at the
+ * origination address. This will fetch the discovery packet of the sender,
+ * which contains the public key we need to verify our guid and url signatures.
+ *
+ * @param array $arr an associative array which must contain:
+ * * \e string \b guid => guid of conversant
+ * * \e string \b guid_sig => guid signed with conversant's private key
+ * * \e string \b url => URL of the origination hub of this communication
+ * * \e string \b url_sig => URL signed with conversant's private key
+ *
+ * @return array An associative array with
+ * * \b success boolean true or false
+ * * \b message (optional) error string only if success is false
+ */
+
+ static function register_hub($id) {
+
+ $id_hash = false;
+ $valid = false;
+ $hsig_valid = false;
+
+ $result = [ 'success' => false ];
+
+ if(! $id) {
+ return $result;
+ }
+
+ $record = Zotfinger::exec($id);
+
+ // Check the HTTP signature
+
+ $hsig = $record['signature'];
+ if($hsig['signer'] === $id && $hsig['header_valid'] === true && $hsig['content_valid'] === true) {
+ $hsig_valid = true;
+ }
+ if(! $hsig_valid) {
+ logger('http signature not valid: ' . print_r($hsig,true));
+ return $result;
+ }
+
+ $c = self::import_xchan($record['data']);
+ if($c['success']) {
+ $result['success'] = true;
+ }
+ else {
+ logger('Failure to verify zot packet');
+ }
+
+ return $result;
+ }
+
+ /**
+ * @brief Takes an associative array of a fetch discovery packet and updates
+ * all internal data structures which need to be updated as a result.
+ *
+ * @param array $arr => json_decoded discovery packet
+ * @param int $ud_flags
+ * Determines whether to create a directory update record if any changes occur, default is UPDATE_FLAGS_UPDATED
+ * $ud_flags = UPDATE_FLAGS_FORCED indicates a forced refresh where we unconditionally create a directory update record
+ * this typically occurs once a month for each channel as part of a scheduled ping to notify the directory
+ * that the channel still exists
+ * @param array $ud_arr
+ * If set [typically by update_directory_entry()] indicates a specific update table row and more particularly
+ * contains a particular address (ud_addr) which needs to be updated in that table.
+ *
+ * @return array An associative array with:
+ * * \e boolean \b success boolean true or false
+ * * \e string \b message (optional) error string only if success is false
+ */
+
+ static function import_xchan($arr, $ud_flags = UPDATE_FLAGS_UPDATED, $ud_arr = null) {
+
+ /**
+ * @hooks import_xchan
+ * Called when processing the result of zot_finger() to store the result
+ * * \e array
+ */
+ call_hooks('import_xchan', $arr);
+
+ $ret = array('success' => false);
+ $dirmode = intval(get_config('system','directory_mode'));
+
+ $changed = false;
+ $what = '';
+
+ if(! ($arr['id'] && $arr['id_sig'])) {
+ logger('No identity information provided. ' . print_r($arr,true));
+ return $ret;
+ }
+
+ $xchan_hash = self::make_xchan_hash($arr['id'],$arr['public_key']);
+ $arr['hash'] = $xchan_hash;
+
+ $import_photos = false;
+
+ $sig_methods = ((array_key_exists('signing',$arr) && is_array($arr['signing'])) ? $arr['signing'] : [ 'sha256' ]);
+ $verified = false;
+
+ if(! self::verify($arr['id'],$arr['id_sig'],$arr['public_key'])) {
+ logger('Unable to verify channel signature for ' . $arr['address']);
+ return $ret;
+ }
+ else {
+ $verified = true;
+ }
+
+ if(! $verified) {
+ $ret['message'] = t('Unable to verify channel signature');
+ return $ret;
+ }
+
+ logger('import_xchan: ' . $xchan_hash, LOGGER_DEBUG);
+
+ $r = q("select * from xchan where xchan_hash = '%s' limit 1",
+ dbesc($xchan_hash)
+ );
+
+ if(! array_key_exists('connect_url', $arr))
+ $arr['connect_url'] = '';
+
+ if($r) {
+ if($arr['photo'] && array_key_exists('updated',$arr['photo']) && $r[0]['xchan_photo_date'] != $arr['photo']['updated']) {
+ $import_photos = true;
+ }
+
+ // if we import an entry from a site that's not ours and either or both of us is off the grid - hide the entry.
+ /** @TODO: check if we're the same directory realm, which would mean we are allowed to see it */
+
+ $dirmode = get_config('system','directory_mode');
+
+ if((($arr['site']['directory_mode'] === 'standalone') || ($dirmode & DIRECTORY_MODE_STANDALONE)) && ($arr['site']['url'] != z_root()))
+ $arr['searchable'] = false;
+
+ $hidden = (1 - intval($arr['searchable']));
+
+ $hidden_changed = $adult_changed = $deleted_changed = $pubforum_changed = 0;
+
+ if(intval($r[0]['xchan_hidden']) != (1 - intval($arr['searchable'])))
+ $hidden_changed = 1;
+ if(intval($r[0]['xchan_selfcensored']) != intval($arr['adult_content']))
+ $adult_changed = 1;
+ if(intval($r[0]['xchan_deleted']) != intval($arr['deleted']))
+ $deleted_changed = 1;
+ if(intval($r[0]['xchan_pubforum']) != intval($arr['public_forum']))
+ $pubforum_changed = 1;
+
+ if($arr['protocols']) {
+ $protocols = implode(',',$arr['protocols']);
+ if($protocols !== 'zot6') {
+ set_xconfig($xchan_hash,'system','protocols',$protocols);
+ }
+ else {
+ del_xconfig($xchan_hash,'system','protocols');
+ }
+ }
+
+ if(($r[0]['xchan_name_date'] != $arr['name_updated'])
+ || ($r[0]['xchan_connurl'] != $arr['primary_location']['connections_url'])
+ || ($r[0]['xchan_addr'] != $arr['primary_location']['address'])
+ || ($r[0]['xchan_follow'] != $arr['primary_location']['follow_url'])
+ || ($r[0]['xchan_connpage'] != $arr['connect_url'])
+ || ($r[0]['xchan_url'] != $arr['primary_location']['url'])
+ || $hidden_changed || $adult_changed || $deleted_changed || $pubforum_changed ) {
+ $rup = q("update xchan set xchan_name = '%s', xchan_name_date = '%s', xchan_connurl = '%s', xchan_follow = '%s',
+ xchan_connpage = '%s', xchan_hidden = %d, xchan_selfcensored = %d, xchan_deleted = %d, xchan_pubforum = %d,
+ xchan_addr = '%s', xchan_url = '%s' where xchan_hash = '%s'",
+ dbesc(($arr['name']) ? escape_tags($arr['name']) : '-'),
+ dbesc($arr['name_updated']),
+ dbesc($arr['primary_location']['connections_url']),
+ dbesc($arr['primary_location']['follow_url']),
+ dbesc($arr['primary_location']['connect_url']),
+ intval(1 - intval($arr['searchable'])),
+ intval($arr['adult_content']),
+ intval($arr['deleted']),
+ intval($arr['public_forum']),
+ dbesc(escape_tags($arr['primary_location']['address'])),
+ dbesc(escape_tags($arr['primary_location']['url'])),
+ dbesc($xchan_hash)
+ );
+
+ logger('Update: existing: ' . print_r($r[0],true), LOGGER_DATA, LOG_DEBUG);
+ logger('Update: new: ' . print_r($arr,true), LOGGER_DATA, LOG_DEBUG);
+ $what .= 'xchan ';
+ $changed = true;
+ }
+ }
+ else {
+ $import_photos = true;
+
+ if((($arr['site']['directory_mode'] === 'standalone')
+ || ($dirmode & DIRECTORY_MODE_STANDALONE))
+ && ($arr['site']['url'] != z_root()))
+ $arr['searchable'] = false;
+
+ $x = xchan_store_lowlevel(
+ [
+ 'xchan_hash' => $xchan_hash,
+ 'xchan_guid' => $arr['id'],
+ 'xchan_guid_sig' => $arr['id_sig'],
+ 'xchan_pubkey' => $arr['public_key'],
+ 'xchan_photo_mimetype' => $arr['photo_mimetype'],
+ 'xchan_photo_l' => $arr['photo'],
+ 'xchan_addr' => escape_tags($arr['primary_location']['address']),
+ 'xchan_url' => escape_tags($arr['primary_location']['url']),
+ 'xchan_connurl' => $arr['primary_location']['connections_url'],
+ 'xchan_follow' => $arr['primary_location']['follow_url'],
+ 'xchan_connpage' => $arr['connect_url'],
+ 'xchan_name' => (($arr['name']) ? escape_tags($arr['name']) : '-'),
+ 'xchan_network' => 'zot6',
+ 'xchan_photo_date' => $arr['photo_updated'],
+ 'xchan_name_date' => $arr['name_updated'],
+ 'xchan_hidden' => intval(1 - intval($arr['searchable'])),
+ 'xchan_selfcensored' => $arr['adult_content'],
+ 'xchan_deleted' => $arr['deleted'],
+ 'xchan_pubforum' => $arr['public_forum']
+ ]
+ );
+
+ $what .= 'new_xchan';
+ $changed = true;
+ }
+
+ if($import_photos) {
+
+ require_once('include/photo/photo_driver.php');
+
+ // see if this is a channel clone that's hosted locally - which we treat different from other xchans/connections
+
+ $local = q("select channel_account_id, channel_id from channel where channel_hash = '%s' limit 1",
+ dbesc($xchan_hash)
+ );
+ if($local) {
+ $ph = z_fetch_url($arr['photo']['url'], true);
+ if($ph['success']) {
+
+ $hash = import_channel_photo($ph['body'], $arr['photo']['type'], $local[0]['channel_account_id'], $local[0]['channel_id']);
+
+ if($hash) {
+ // unless proven otherwise
+ $is_default_profile = 1;
+
+ $profile = q("select is_default from profile where aid = %d and uid = %d limit 1",
+ intval($local[0]['channel_account_id']),
+ intval($local[0]['channel_id'])
+ );
+ if($profile) {
+ if(! intval($profile[0]['is_default']))
+ $is_default_profile = 0;
+ }
+
+ // If setting for the default profile, unset the profile photo flag from any other photos I own
+ if($is_default_profile) {
+ q("UPDATE photo SET photo_usage = %d WHERE photo_usage = %d AND resource_id != '%s' AND aid = %d AND uid = %d",
+ intval(PHOTO_NORMAL),
+ intval(PHOTO_PROFILE),
+ dbesc($hash),
+ intval($local[0]['channel_account_id']),
+ intval($local[0]['channel_id'])
+ );
+ }
+ }
+
+ // reset the names in case they got messed up when we had a bug in this function
+ $photos = array(
+ z_root() . '/photo/profile/l/' . $local[0]['channel_id'],
+ z_root() . '/photo/profile/m/' . $local[0]['channel_id'],
+ z_root() . '/photo/profile/s/' . $local[0]['channel_id'],
+ $arr['photo_mimetype'],
+ false
+ );
+ }
+ }
+ else {
+ $photos = import_xchan_photo($arr['photo']['url'], $xchan_hash);
+ }
+ if($photos) {
+ if($photos[4]) {
+ // importing the photo failed somehow. Leave the photo_date alone so we can try again at a later date.
+ // This often happens when somebody joins the matrix with a bad cert.
+ $r = 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($xchan_hash)
+ );
+ }
+ else {
+ $r = q("update xchan set xchan_photo_date = '%s', xchan_photo_l = '%s', xchan_photo_m = '%s', xchan_photo_s = '%s', xchan_photo_mimetype = '%s'
+ where xchan_hash = '%s'",
+ dbescdate(datetime_convert('UTC','UTC',$arr['photo_updated'])),
+ dbesc($photos[0]),
+ dbesc($photos[1]),
+ dbesc($photos[2]),
+ dbesc($photos[3]),
+ dbesc($xchan_hash)
+ );
+ }
+ $what .= 'photo ';
+ $changed = true;
+ }
+ }
+
+ // what we are missing for true hub independence is for any changes in the primary hub to
+ // get reflected not only in the hublocs, but also to update the URLs and addr in the appropriate xchan
+
+ $s = Libsync::sync_locations($arr, $arr);
+
+ if($s) {
+ if($s['change_message'])
+ $what .= $s['change_message'];
+ if($s['changed'])
+ $changed = $s['changed'];
+ if($s['message'])
+ $ret['message'] .= $s['message'];
+ }
+
+ // Which entries in the update table are we interested in updating?
+
+ $address = (($ud_arr && $ud_arr['ud_addr']) ? $ud_arr['ud_addr'] : $arr['address']);
+
+
+ // Are we a directory server of some kind?
+
+ $other_realm = false;
+ $realm = get_directory_realm();
+ if(array_key_exists('site',$arr)
+ && array_key_exists('realm',$arr['site'])
+ && (strpos($arr['site']['realm'],$realm) === false))
+ $other_realm = true;
+
+
+ if($dirmode != DIRECTORY_MODE_NORMAL) {
+
+ // We're some kind of directory server. However we can only add directory information
+ // if the entry is in the same realm (or is a sub-realm). Sub-realms are denoted by
+ // including the parent realm in the name. e.g. 'RED_GLOBAL:foo' would allow an entry to
+ // be in directories for the local realm (foo) and also the RED_GLOBAL realm.
+
+ if(array_key_exists('profile',$arr) && is_array($arr['profile']) && (! $other_realm)) {
+ $profile_changed = Libzotdir::import_directory_profile($xchan_hash,$arr['profile'],$address,$ud_flags, 1);
+ if($profile_changed) {
+ $what .= 'profile ';
+ $changed = true;
+ }
+ }
+ else {
+ logger('Profile not available - hiding');
+ // they may have made it private
+ $r = q("delete from xprof where xprof_hash = '%s'",
+ dbesc($xchan_hash)
+ );
+ $r = q("delete from xtag where xtag_hash = '%s' and xtag_flags = 0",
+ dbesc($xchan_hash)
+ );
+ }
+ }
+
+ if(array_key_exists('site',$arr) && is_array($arr['site'])) {
+ $profile_changed = self::import_site($arr['site']);
+ if($profile_changed) {
+ $what .= 'site ';
+ $changed = true;
+ }
+ }
+
+ if(($changed) || ($ud_flags == UPDATE_FLAGS_FORCED)) {
+ $guid = random_string() . '@' . \App::get_hostname();
+ Libzotdir::update_modtime($xchan_hash,$guid,$address,$ud_flags);
+ logger('Changed: ' . $what,LOGGER_DEBUG);
+ }
+ elseif(! $ud_flags) {
+ // nothing changed but we still need to update the updates record
+ q("update updates set ud_flags = ( ud_flags | %d ) where ud_addr = '%s' and not (ud_flags & %d) > 0 ",
+ intval(UPDATE_FLAGS_UPDATED),
+ dbesc($address),
+ intval(UPDATE_FLAGS_UPDATED)
+ );
+ }
+
+ if(! x($ret,'message')) {
+ $ret['success'] = true;
+ $ret['hash'] = $xchan_hash;
+ }
+
+ logger('Result: ' . print_r($ret,true), LOGGER_DATA, LOG_DEBUG);
+ return $ret;
+ }
+
+ /**
+ * @brief Called immediately after sending a zot message which is using queue processing.
+ *
+ * Updates the queue item according to the response result and logs any information
+ * returned to aid communications troubleshooting.
+ *
+ * @param string $hub - url of site we just contacted
+ * @param array $arr - output of z_post_url()
+ * @param array $outq - The queue structure attached to this request
+ */
+
+ static function process_response($hub, $arr, $outq) {
+
+ logger('remote: ' . print_r($arr,true),LOGGER_DATA);
+
+ if(! $arr['success']) {
+ logger('Failed: ' . $hub);
+ return;
+ }
+
+ $x = json_decode($arr['body'], true);
+
+ if(! $x) {
+ logger('No json from ' . $hub);
+ logger('Headers: ' . print_r($arr['header'], true), LOGGER_DATA, LOG_DEBUG);
+ }
+
+ $x = crypto_unencapsulate($x, get_config('system','prvkey'));
+ if(! is_array($x)) {
+ $x = json_decode($x,true);
+ }
+
+ if(! $x['success']) {
+
+ // handle remote validation issues
+
+ $b = q("update dreport set dreport_result = '%s', dreport_time = '%s' where dreport_queue = '%s'",
+ dbesc(($x['message']) ? $x['message'] : 'unknown delivery error'),
+ dbesc(datetime_convert()),
+ dbesc($outq['outq_hash'])
+ );
+ }
+
+ if(array_key_exists('delivery_report',$x) && is_array($x['delivery_report'])) {
+ foreach($x['delivery_report'] as $xx) {
+ if(is_array($xx) && array_key_exists('message_id',$xx) && DReport::is_storable($xx)) {
+ q("insert into dreport ( dreport_mid, dreport_site, dreport_recip, dreport_name, dreport_result, dreport_time, dreport_xchan ) values ( '%s', '%s', '%s','%s','%s','%s','%s' ) ",
+ dbesc($xx['message_id']),
+ dbesc($xx['location']),
+ dbesc($xx['recipient']),
+ dbesc($xx['name']),
+ dbesc($xx['status']),
+ dbesc(datetime_convert($xx['date'])),
+ dbesc($xx['sender'])
+ );
+ }
+ }
+
+ // we have a more descriptive delivery report, so discard the per hub 'queue' report.
+
+ q("delete from dreport where dreport_queue = '%s' ",
+ dbesc($outq['outq_hash'])
+ );
+ }
+
+ // update the timestamp for this site
+
+ q("update site set site_dead = 0, site_update = '%s' where site_url = '%s'",
+ dbesc(datetime_convert()),
+ dbesc(dirname($hub))
+ );
+
+ // synchronous message types are handled immediately
+ // async messages remain in the queue until processed.
+
+ if(intval($outq['outq_async']))
+ Queue::remove($outq['outq_hash'],$outq['outq_channel']);
+
+ logger('zot_process_response: ' . print_r($x,true), LOGGER_DEBUG);
+ }
+
+ /**
+ * @brief
+ *
+ * We received a notification packet (in mod_post) that a message is waiting for us, and we've verified the sender.
+ * Check if the site is using zot6 delivery and includes a verified HTTP Signature, signed content, and a 'msg' field,
+ * and also that the signer and the sender match.
+ * If that happens, we do not need to fetch/pickup the message - we have it already and it is verified.
+ * Translate it into the form we need for zot_import() and import it.
+ *
+ * Otherwise send back a pickup message, using our message tracking ID ($arr['secret']), which we will sign with our site
+ * private key.
+ * The entire pickup message is encrypted with the remote site's public key.
+ * If everything checks out on the remote end, we will receive back a packet containing one or more messages,
+ * which will be processed and delivered before this function ultimately returns.
+ *
+ * @see zot_import()
+ *
+ * @param array $arr
+ * decrypted and json decoded notify packet from remote site
+ * @return array from zot_import()
+ */
+
+ static function fetch($arr) {
+
+ logger('zot_fetch: ' . print_r($arr,true), LOGGER_DATA, LOG_DEBUG);
+
+ return self::import($arr);
+
+ }
+
+ /**
+ * @brief Process incoming array of messages.
+ *
+ * Process an incoming array of messages which were obtained via pickup, and
+ * import, update, delete as directed.
+ *
+ * The message types handled here are 'activity' (e.g. posts), and 'sync'.
+ *
+ * @param array $arr
+ * 'pickup' structure returned from remote site
+ * @param string $sender_url
+ * the url specified by the sender in the initial communication.
+ * We will verify the sender and url in each returned message structure and
+ * also verify that all the messages returned match the site url that we are
+ * currently processing.
+ *
+ * @returns array
+ * Suitable for logging remotely, enumerating the processing results of each message/recipient combination
+ * * [0] => \e string $channel_hash
+ * * [1] => \e string $delivery_status
+ * * [2] => \e string $address
+ */
+
+ static function import($arr) {
+
+ $env = $arr;
+ $private = false;
+ $return = [];
+
+ $result = null;
+
+ logger('Notify: ' . print_r($env,true), LOGGER_DATA, LOG_DEBUG);
+
+ if(! is_array($env)) {
+ logger('decode error');
+ return;
+ }
+
+ $message_request = ((array_key_exists('message_id',$env)) ? true : false);
+ if($message_request)
+ logger('processing message request');
+
+ $has_data = array_key_exists('data',$env) && $env['data'];
+ $data = (($has_data) ? $env['data'] : false);
+
+ $deliveries = null;
+
+ if(array_key_exists('recipients',$env) && count($env['recipients'])) {
+ logger('specific recipients');
+ logger('recipients: ' . print_r($env['recipients'],true),LOGGER_DEBUG);
+
+ $recip_arr = [];
+ foreach($env['recipients'] as $recip) {
+ $recip_arr[] = $recip;
+ }
+
+ $r = false;
+ if($recip_arr) {
+ stringify_array_elms($recip_arr,true);
+ $recips = implode(',',$recip_arr);
+ $r = q("select channel_hash as hash from channel where channel_hash in ( " . $recips . " ) and channel_removed = 0 ");
+ }
+
+ if(! $r) {
+ logger('recips: no recipients on this site');
+ return;
+ }
+
+ // Response messages will inherit the privacy of the parent
+
+ if($env['type'] !== 'response')
+ $private = true;
+
+ $deliveries = ids_to_array($r,'hash');
+
+ // We found somebody on this site that's in the recipient list.
+ }
+ else {
+
+ logger('public post');
+
+
+ // Public post. look for any site members who are or may be accepting posts from this sender
+ // and who are allowed to see them based on the sender's permissions
+ // @fixme;
+
+ $deliveries = self::public_recips($env);
+
+
+ }
+
+ $deliveries = array_unique($deliveries);
+
+ if(! $deliveries) {
+ logger('No deliveries on this site');
+ return;
+ }
+
+
+ if($has_data) {
+
+ if(in_array($env['type'],['activity','response'])) {
+
+ if($env['encoding'] === 'zot') {
+ $arr = get_item_elements($data);
+
+ $v = validate_item_elements($data,$arr);
+
+ if(! $v['success']) {
+ logger('Activity rejected: ' . $v['message'] . ' ' . print_r($data,true));
+ return;
+ }
+ }
+ elseif($env['encoding'] === 'activitystreams') {
+
+ $AS = new \Zotlabs\Lib\ActivityStreams($data);
+ if(! $AS->is_valid()) {
+ logger('Activity rejected: ' . print_r($data,true));
+ return;
+ }
+ $arr = \Zotlabs\Lib\Activity::decode_note($AS);
+
+ logger($AS->debug());
+
+ $r = q("select hubloc_hash from hubloc where hubloc_id_url = '%s' limit 1",
+ dbesc($AS->actor['id'])
+ );
+
+ if($r) {
+ $arr['author_xchan'] = $r[0]['hubloc_hash'];
+ }
+ // @fixme (in individual delivery, change owner if needed)
+ $arr['owner_xchan'] = $env['sender'];
+ if($private) {
+ $arr['item_private'] = true;
+ }
+ // @fixme - spoofable
+ if($AS->data['hubloc']) {
+ $arr['item_verified'] = true;
+ }
+ if($AS->data['signed_data']) {
+ IConfig::Set($arr,'activitystreams','signed_data',$AS->data['signed_data'],false);
+ }
+
+ }
+
+ logger('Activity received: ' . print_r($arr,true), LOGGER_DATA, LOG_DEBUG);
+ logger('Activity recipients: ' . print_r($deliveries,true), LOGGER_DATA, LOG_DEBUG);
+
+ $relay = (($env['type'] === 'response') ? true : false );
+
+ $result = self::process_delivery($env['sender'],$arr,$deliveries,$relay,false,$message_request);
+ }
+ elseif($env['type'] === 'sync') {
+ // $arr = get_channelsync_elements($data);
+
+ $arr = json_decode($data,true);
+
+ logger('Channel sync received: ' . print_r($arr,true), LOGGER_DATA, LOG_DEBUG);
+ logger('Channel sync recipients: ' . print_r($deliveries,true), LOGGER_DATA, LOG_DEBUG);
+
+ $result = Libsync::process_channel_sync_delivery($env['sender'],$arr,$deliveries);
+ }
+ }
+ if ($result) {
+ $return = array_merge($return, $result);
+ }
+ return $return;
+ }
+
+
+ static function is_top_level($env) {
+ if($env['encoding'] === 'zot' && array_key_exists('flags',$env) && in_array('thread_parent', $env['flags'])) {
+ return true;
+ }
+ if($env['encoding'] === 'activitystreams') {
+ if(array_key_exists('inReplyTo',$env['data']) && $env['data']['inReplyTo']) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * @brief
+ *
+ * A public message with no listed recipients can be delivered to anybody who
+ * has PERMS_NETWORK for that type of post, PERMS_AUTHED (in-network senders are
+ * by definition authenticated) or PERMS_SITE and is one the same site,
+ * or PERMS_SPECIFIC and the sender is a contact who is granted permissions via
+ * their connection permissions in the address book.
+ * Here we take a given message and construct a list of hashes of everybody
+ * on the site that we should try and deliver to.
+ * Some of these will be rejected, but this gives us a place to start.
+ *
+ * @param array $msg
+ * @return NULL|array
+ */
+
+ static function public_recips($msg) {
+
+ require_once('include/channel.php');
+
+ $check_mentions = false;
+ $include_sys = false;
+
+ if($msg['type'] === 'activity') {
+ $disable_discover_tab = get_config('system','disable_discover_tab') || get_config('system','disable_discover_tab') === false;
+ if(! $disable_discover_tab)
+ $include_sys = true;
+
+ $perm = 'send_stream';
+
+ if(self::is_top_level($msg)) {
+ $check_mentions = true;
+ }
+ }
+ elseif($msg['type'] === 'mail')
+ $perm = 'post_mail';
+
+ $r = [];
+
+ $c = q("select channel_id, channel_hash from channel where channel_removed = 0");
+
+ if($c) {
+ foreach($c as $cc) {
+ if(perm_is_allowed($cc['channel_id'],$msg['sender'],$perm)) {
+ $r[] = $cc['channel_hash'];
+ }
+ }
+ }
+
+ if($include_sys) {
+ $sys = get_sys_channel();
+ if($sys)
+ $r[] = $sys['channel_hash'];
+ }
+
+
+
+ // look for any public mentions on this site
+ // They will get filtered by tgroup_check() so we don't need to check permissions now
+
+ if($check_mentions) {
+ // It's a top level post. Look at the tags. See if any of them are mentions and are on this hub.
+ if(array_path_exists('data/object/tag',$msg)) {
+ if(is_array($msg['data']['object']['tag']) && $msg['data']['object']['tag']) {
+ foreach($msg['data']['object']['tag'] as $tag) {
+ if($tag['type'] === 'Mention' && (strpos($tag['href'],z_root()) !== false)) {
+ $address = basename($tag['href']);
+ if($address) {
+ $z = q("select channel_hash as hash from channel where channel_address = '%s'
+ and channel_removed = 0 limit 1",
+ dbesc($address)
+ );
+ if($z) {
+ $r[] = $z[0]['hash'];
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ else {
+ // This is a comment. We need to find any parent with ITEM_UPLINK set. But in fact, let's just return
+ // everybody that stored a copy of the parent. This way we know we're covered. We'll check the
+ // comment permissions when we deliver them.
+
+ if(array_path_exists('data/inReplyTo',$msg)) {
+ $z = q("select owner_xchan as hash from item where parent_mid = '%s' ",
+ dbesc($msg['data']['inReplyTo'])
+ );
+ if($z) {
+ foreach($z as $zv) {
+ $r[] = $zv['hash'];
+ }
+ }
+ }
+ }
+
+ // There are probably a lot of duplicates in $r at this point. We need to filter those out.
+ // It's a bit of work since it's a multi-dimensional array
+
+ if($r) {
+ $r = array_unique($r);
+ }
+
+ logger('public_recips: ' . print_r($r,true), LOGGER_DATA, LOG_DEBUG);
+ return $r;
+ }
+
+
+ /**
+ * @brief
+ *
+ * @param array $sender
+ * @param array $arr
+ * @param array $deliveries
+ * @param boolean $relay
+ * @param boolean $public (optional) default false
+ * @param boolean $request (optional) default false
+ * @return array
+ */
+
+ static function process_delivery($sender, $arr, $deliveries, $relay, $public = false, $request = false) {
+
+ $result = [];
+
+ // We've validated the sender. Now make sure that the sender is the owner or author
+
+ if(! $public) {
+ if($sender != $arr['owner_xchan'] && $sender != $arr['author_xchan']) {
+ logger("Sender $sender is not owner {$arr['owner_xchan']} or author {$arr['author_xchan']} - mid {$arr['mid']}");
+ return;
+ }
+ }
+
+ foreach($deliveries as $d) {
+
+ $local_public = $public;
+
+ $DR = new \Zotlabs\Lib\DReport(z_root(),$sender,$d,$arr['mid']);
+
+ $channel = channelx_by_hash($d);
+
+ if (! $channel) {
+ $DR->update('recipient not found');
+ $result[] = $DR->get();
+ continue;
+ }
+
+ $DR->set_name($channel['channel_name'] . ' <' . channel_reddress($channel) . '>');
+
+ /**
+ * We need to block normal top-level message delivery from our clones, as the delivered
+ * message doesn't have ACL information in it as the cloned copy does. That copy
+ * will normally arrive first via sync delivery, but this isn't guaranteed.
+ * There's a chance the current delivery could take place before the cloned copy arrives
+ * hence the item could have the wrong ACL and *could* be used in subsequent deliveries or
+ * access checks.
+ */
+
+ if($sender === $channel['channel_hash'] && $arr['author_xchan'] === $channel['channel_hash'] && $arr['mid'] === $arr['parent_mid']) {
+ $DR->update('self delivery ignored');
+ $result[] = $DR->get();
+ continue;
+ }
+
+ // allow public postings to the sys channel regardless of permissions, but not
+ // for comments travelling upstream. Wait and catch them on the way down.
+ // They may have been blocked by the owner.
+
+ if(intval($channel['channel_system']) && (! $arr['item_private']) && (! $relay)) {
+ $local_public = true;
+
+ $r = q("select xchan_selfcensored from xchan where xchan_hash = '%s' limit 1",
+ dbesc($sender['hash'])
+ );
+ // don't import sys channel posts from selfcensored authors
+ if($r && (intval($r[0]['xchan_selfcensored']))) {
+ $local_public = false;
+ continue;
+ }
+ if(! MessageFilter::evaluate($arr,get_config('system','pubstream_incl'),get_config('system','pubstream_excl'))) {
+ $local_public = false;
+ continue;
+ }
+ }
+
+ $tag_delivery = tgroup_check($channel['channel_id'],$arr);
+
+ $perm = 'send_stream';
+ if(($arr['mid'] !== $arr['parent_mid']) && ($relay))
+ $perm = 'post_comments';
+
+ // This is our own post, possibly coming from a channel clone
+
+ if($arr['owner_xchan'] == $d) {
+ $arr['item_wall'] = 1;
+ }
+ else {
+ $arr['item_wall'] = 0;
+ }
+
+ if((! perm_is_allowed($channel['channel_id'],$sender,$perm)) && (! $tag_delivery) && (! $local_public)) {
+ logger("permission denied for delivery to channel {$channel['channel_id']} {$channel['channel_address']}");
+ $DR->update('permission denied');
+ $result[] = $DR->get();
+ continue;
+ }
+
+ if($arr['mid'] != $arr['parent_mid']) {
+
+ // check source route.
+ // We are only going to accept comments from this sender if the comment has the same route as the top-level-post,
+ // this is so that permissions mismatches between senders apply to the entire conversation
+ // As a side effect we will also do a preliminary check that we have the top-level-post, otherwise
+ // processing it is pointless.
+
+ $r = q("select route, id from item where mid = '%s' and uid = %d limit 1",
+ dbesc($arr['parent_mid']),
+ intval($channel['channel_id'])
+ );
+ if(! $r) {
+ $DR->update('comment parent not found');
+ $result[] = $DR->get();
+
+ // We don't seem to have a copy of this conversation or at least the parent
+ // - so request a copy of the entire conversation to date.
+ // Don't do this if it's a relay post as we're the ones who are supposed to
+ // have the copy and we don't want the request to loop.
+ // Also don't do this if this comment came from a conversation request packet.
+ // It's possible that comments are allowed but posting isn't and that could
+ // cause a conversation fetch loop. We can detect these packets since they are
+ // delivered via a 'notify' packet type that has a message_id element in the
+ // initial zot packet (just like the corresponding 'request' packet type which
+ // makes the request).
+ // We'll also check the send_stream permission - because if it isn't allowed,
+ // the top level post is unlikely to be imported and
+ // this is just an exercise in futility.
+
+ if((! $relay) && (! $request) && (! $local_public)
+ && perm_is_allowed($channel['channel_id'],$sender,'send_stream')) {
+ \Zotlabs\Daemon\Master::Summon(array('Notifier', 'request', $channel['channel_id'], $sender, $arr['parent_mid']));
+ }
+ continue;
+ }
+ if($relay) {
+ // reset the route in case it travelled a great distance upstream
+ // use our parent's route so when we go back downstream we'll match
+ // with whatever route our parent has.
+ $arr['route'] = $r[0]['route'];
+ }
+ else {
+
+ // going downstream check that we have the same upstream provider that
+ // sent it to us originally. Ignore it if it came from another source
+ // (with potentially different permissions).
+ // only compare the last hop since it could have arrived at the last location any number of ways.
+ // Always accept empty routes and firehose items (route contains 'undefined') .
+
+ $existing_route = explode(',', $r[0]['route']);
+ $routes = count($existing_route);
+ if($routes) {
+ $last_hop = array_pop($existing_route);
+ $last_prior_route = implode(',',$existing_route);
+ }
+ else {
+ $last_hop = '';
+ $last_prior_route = '';
+ }
+
+ if(in_array('undefined',$existing_route) || $last_hop == 'undefined' || $sender == 'undefined')
+ $last_hop = '';
+
+ $current_route = (($arr['route']) ? $arr['route'] . ',' : '') . $sender;
+
+ if($last_hop && $last_hop != $sender) {
+ logger('comment route mismatch: parent route = ' . $r[0]['route'] . ' expected = ' . $current_route, LOGGER_DEBUG);
+ logger('comment route mismatch: parent msg = ' . $r[0]['id'],LOGGER_DEBUG);
+ $DR->update('comment route mismatch');
+ $result[] = $DR->get();
+ continue;
+ }
+
+ // we'll add sender onto this when we deliver it. $last_prior_route now has the previously stored route
+ // *except* for the sender which would've been the last hop before it got to us.
+
+ $arr['route'] = $last_prior_route;
+ }
+ }
+
+ $ab = q("select * from abook where abook_channel = %d and abook_xchan = '%s'",
+ intval($channel['channel_id']),
+ dbesc($arr['owner_xchan'])
+ );
+ $abook = (($ab) ? $ab[0] : null);
+
+ if(intval($arr['item_deleted'])) {
+
+ // remove_community_tag is a no-op if this isn't a community tag activity
+ self::remove_community_tag($sender,$arr,$channel['channel_id']);
+
+ // set these just in case we need to store a fresh copy of the deleted post.
+ // This could happen if the delete got here before the original post did.
+
+ $arr['aid'] = $channel['channel_account_id'];
+ $arr['uid'] = $channel['channel_id'];
+
+ $item_id = delete_imported_item($sender,$arr,$channel['channel_id'],$relay);
+ $DR->update(($item_id) ? 'deleted' : 'delete_failed');
+ $result[] = $DR->get();
+
+ if($relay && $item_id) {
+ logger('process_delivery: invoking relay');
+ \Zotlabs\Daemon\Master::Summon(array('Notifier','relay',intval($item_id)));
+ $DR->update('relayed');
+ $result[] = $DR->get();
+ }
+
+ continue;
+ }
+
+
+ $r = q("select * from item where mid = '%s' and uid = %d limit 1",
+ dbesc($arr['mid']),
+ intval($channel['channel_id'])
+ );
+ if($r) {
+ // We already have this post.
+ $item_id = $r[0]['id'];
+
+ if(intval($r[0]['item_deleted'])) {
+ // It was deleted locally.
+ $DR->update('update ignored');
+ $result[] = $DR->get();
+
+ continue;
+ }
+ // Maybe it has been edited?
+ elseif($arr['edited'] > $r[0]['edited']) {
+ $arr['id'] = $r[0]['id'];
+ $arr['uid'] = $channel['channel_id'];
+ if(($arr['mid'] == $arr['parent_mid']) && (! post_is_importable($arr,$abook))) {
+ $DR->update('update ignored');
+ $result[] = $DR->get();
+ }
+ else {
+ $item_result = self::update_imported_item($sender,$arr,$r[0],$channel['channel_id'],$tag_delivery);
+ $DR->update('updated');
+ $result[] = $DR->get();
+ if(! $relay)
+ add_source_route($item_id,$sender);
+ }
+ }
+ else {
+ $DR->update('update ignored');
+ $result[] = $DR->get();
+
+ // We need this line to ensure wall-to-wall comments are relayed (by falling through to the relay bit),
+ // and at the same time not relay any other relayable posts more than once, because to do so is very wasteful.
+ if(! intval($r[0]['item_origin']))
+ continue;
+ }
+ }
+ else {
+ $arr['aid'] = $channel['channel_account_id'];
+ $arr['uid'] = $channel['channel_id'];
+
+ // if it's a sourced post, call the post_local hooks as if it were
+ // posted locally so that crosspost connectors will be triggered.
+
+ if(check_item_source($arr['uid'], $arr)) {
+ /**
+ * @hooks post_local
+ * Called when an item has been posted on this machine via mod/item.php (also via API).
+ * * \e array with an item
+ */
+ call_hooks('post_local', $arr);
+ }
+
+ $item_id = 0;
+
+ if(($arr['mid'] == $arr['parent_mid']) && (! post_is_importable($arr,$abook))) {
+ $DR->update('post ignored');
+ $result[] = $DR->get();
+ }
+ else {
+ $item_result = item_store($arr);
+ if($item_result['success']) {
+ $item_id = $item_result['item_id'];
+ $parr = [
+ 'item_id' => $item_id,
+ 'item' => $arr,
+ 'sender' => $sender,
+ 'channel' => $channel
+ ];
+ /**
+ * @hooks activity_received
+ * Called when an activity (post, comment, like, etc.) has been received from a zot source.
+ * * \e int \b item_id
+ * * \e array \b item
+ * * \e array \b sender
+ * * \e array \b channel
+ */
+ call_hooks('activity_received', $parr);
+ // don't add a source route if it's a relay or later recipients will get a route mismatch
+ if(! $relay)
+ add_source_route($item_id,$sender);
+ }
+ $DR->update(($item_id) ? 'posted' : 'storage failed: ' . $item_result['message']);
+ $result[] = $DR->get();
+ }
+ }
+
+ // preserve conversations with which you are involved from expiration
+
+ $stored = (($item_result && $item_result['item']) ? $item_result['item'] : false);
+ if((is_array($stored)) && ($stored['id'] != $stored['parent'])
+ && ($stored['author_xchan'] === $channel['channel_hash'])) {
+ retain_item($stored['item']['parent']);
+ }
+
+ if($relay && $item_id) {
+ logger('Invoking relay');
+ \Zotlabs\Daemon\Master::Summon(array('Notifier','relay',intval($item_id)));
+ $DR->addto_update('relayed');
+ $result[] = $DR->get();
+ }
+ }
+
+ if(! $deliveries)
+ $result[] = array('', 'no recipients', '', $arr['mid']);
+
+ logger('Local results: ' . print_r($result, true), LOGGER_DEBUG);
+
+ return $result;
+ }
+
+ /**
+ * @brief Remove community tag.
+ *
+ * @param array $sender an associative array with
+ * * \e string \b hash a xchan_hash
+ * @param array $arr an associative array
+ * * \e int \b verb
+ * * \e int \b obj_type
+ * * \e int \b mid
+ * @param int $uid
+ */
+
+ static function remove_community_tag($sender, $arr, $uid) {
+
+ if(! (activity_match($arr['verb'], ACTIVITY_TAG) && ($arr['obj_type'] == ACTIVITY_OBJ_TAGTERM)))
+ return;
+
+ logger('remove_community_tag: invoked');
+
+ if(! get_pconfig($uid,'system','blocktags')) {
+ logger('Permission denied.');
+ return;
+ }
+
+ $r = q("select * from item where mid = '%s' and uid = %d limit 1",
+ dbesc($arr['mid']),
+ intval($uid)
+ );
+ if(! $r) {
+ logger('No item');
+ return;
+ }
+
+ if(($sender != $r[0]['owner_xchan']) && ($sender != $r[0]['author_xchan'])) {
+ logger('Sender not authorised.');
+ return;
+ }
+
+ $i = $r[0];
+
+ if($i['target'])
+ $i['target'] = json_decode($i['target'],true);
+ if($i['object'])
+ $i['object'] = json_decode($i['object'],true);
+
+ if(! ($i['target'] && $i['object'])) {
+ logger('No target/object');
+ return;
+ }
+
+ $message_id = $i['target']['id'];
+
+ $r = q("select id from item where mid = '%s' and uid = %d limit 1",
+ dbesc($message_id),
+ intval($uid)
+ );
+ if(! $r) {
+ logger('No parent message');
+ return;
+ }
+
+ q("delete from term where uid = %d and oid = %d and otype = %d and ttype in ( %d, %d ) and term = '%s' and url = '%s'",
+ intval($uid),
+ intval($r[0]['id']),
+ intval(TERM_OBJ_POST),
+ intval(TERM_HASHTAG),
+ intval(TERM_COMMUNITYTAG),
+ dbesc($i['object']['title']),
+ dbesc(get_rel_link($i['object']['link'],'alternate'))
+ );
+ }
+
+ /**
+ * @brief Updates an imported item.
+ *
+ * @see item_store_update()
+ *
+ * @param array $sender
+ * @param array $item
+ * @param array $orig
+ * @param int $uid
+ * @param boolean $tag_delivery
+ */
+
+ static function update_imported_item($sender, $item, $orig, $uid, $tag_delivery) {
+
+ // If this is a comment being updated, remove any privacy information
+ // so that item_store_update will set it from the original.
+
+ if($item['mid'] !== $item['parent_mid']) {
+ unset($item['allow_cid']);
+ unset($item['allow_gid']);
+ unset($item['deny_cid']);
+ unset($item['deny_gid']);
+ unset($item['item_private']);
+ }
+
+ // we need the tag_delivery check for downstream flowing posts as the stored post
+ // may have a different owner than the one being transmitted.
+
+ if(($sender != $orig['owner_xchan'] && $sender != $orig['author_xchan']) && (! $tag_delivery)) {
+ logger('sender is not owner or author');
+ return;
+ }
+
+
+ $x = item_store_update($item);
+
+ // If we're updating an event that we've saved locally, we store the item info first
+ // because event_addtocal will parse the body to get the 'new' event details
+
+ if($orig['resource_type'] === 'event') {
+ $res = event_addtocal($orig['id'], $uid);
+ if(! $res)
+ logger('update event: failed');
+ }
+
+ if(! $x['item_id'])
+ logger('update_imported_item: failed: ' . $x['message']);
+ else
+ logger('update_imported_item');
+
+ return $x;
+ }
+
+ /**
+ * @brief Deletes an imported item.
+ *
+ * @param array $sender
+ * * \e string \b hash a xchan_hash
+ * @param array $item
+ * @param int $uid
+ * @param boolean $relay
+ * @return boolean|int post_id
+ */
+
+ static function delete_imported_item($sender, $item, $uid, $relay) {
+
+ logger('invoked', LOGGER_DEBUG);
+
+ $ownership_valid = false;
+ $item_found = false;
+ $post_id = 0;
+
+ $r = q("select id, author_xchan, owner_xchan, source_xchan, item_deleted from item where ( author_xchan = '%s' or owner_xchan = '%s' or source_xchan = '%s' )
+ and mid = '%s' and uid = %d limit 1",
+ dbesc($sender['hash']),
+ dbesc($sender['hash']),
+ dbesc($sender['hash']),
+ dbesc($item['mid']),
+ intval($uid)
+ );
+
+ if($r) {
+ if($r[0]['author_xchan'] === $sender || $r[0]['owner_xchan'] === $sender || $r[0]['source_xchan'] === $sender)
+ $ownership_valid = true;
+
+ $post_id = $r[0]['id'];
+ $item_found = true;
+ }
+ else {
+
+ // perhaps the item is still in transit and the delete notification got here before the actual item did. Store it with the deleted flag set.
+ // item_store() won't try to deliver any notifications or start delivery chains if this flag is set.
+ // This means we won't end up with potentially even more delivery threads trying to push this delete notification.
+ // But this will ensure that if the (undeleted) original post comes in at a later date, we'll reject it because it will have an older timestamp.
+
+ logger('delete received for non-existent item - storing item data.');
+
+ if($item['author_xchan'] === $sender || $item['owner_xchan'] === $sender || $item['source_xchan'] === $sender) {
+ $ownership_valid = true;
+ $item_result = item_store($item);
+ $post_id = $item_result['item_id'];
+ }
+ }
+
+ if($ownership_valid === false) {
+ logger('delete_imported_item: failed: ownership issue');
+ return false;
+ }
+
+ if($item_found) {
+ if(intval($r[0]['item_deleted'])) {
+ logger('delete_imported_item: item was already deleted');
+ if(! $relay)
+ return false;
+
+ // This is a bit hackish, but may have to suffice until the notification/delivery loop is optimised
+ // a bit further. We're going to strip the ITEM_ORIGIN on this item if it's a comment, because
+ // it was already deleted, and we're already relaying, and this ensures that no other process or
+ // code path downstream can relay it again (causing a loop). Since it's already gone it's not coming
+ // back, and we aren't going to (or shouldn't at any rate) delete it again in the future - so losing
+ // this information from the metadata should have no other discernible impact.
+
+ if (($r[0]['id'] != $r[0]['parent']) && intval($r[0]['item_origin'])) {
+ q("update item set item_origin = 0 where id = %d and uid = %d",
+ intval($r[0]['id']),
+ intval($r[0]['uid'])
+ );
+ }
+ }
+
+
+ // Use phased deletion to set the deleted flag, call both tag_deliver and the notifier to notify downstream channels
+ // and then clean up after ourselves with a cron job after several days to do the delete_item_lowlevel() (DROPITEM_PHASE2).
+
+ drop_item($post_id, false, DROPITEM_PHASE1);
+ tag_deliver($uid, $post_id);
+ }
+
+ return $post_id;
+ }
+
+ static function process_mail_delivery($sender, $arr, $deliveries) {
+
+ $result = array();
+
+ if($sender != $arr['from_xchan']) {
+ logger('process_mail_delivery: sender is not mail author');
+ return;
+ }
+
+ foreach($deliveries as $d) {
+
+ $DR = new \Zotlabs\Lib\DReport(z_root(),$sender,$d,$arr['mid']);
+
+ $r = q("select * from channel where channel_hash = '%s' limit 1",
+ dbesc($d['hash'])
+ );
+
+ if(! $r) {
+ $DR->update('recipient not found');
+ $result[] = $DR->get();
+ continue;
+ }
+
+ $channel = $r[0];
+ $DR->set_name($channel['channel_name'] . ' <' . channel_reddress($channel) . '>');
+
+
+ if(! perm_is_allowed($channel['channel_id'],$sender,'post_mail')) {
+
+ /*
+ * Always allow somebody to reply if you initiated the conversation. It's anti-social
+ * and a bit rude to send a private message to somebody and block their ability to respond.
+ * If you are being harrassed and want to put an end to it, delete the conversation.
+ */
+
+ $return = false;
+ if($arr['parent_mid']) {
+ $return = q("select * from mail where mid = '%s' and channel_id = %d limit 1",
+ dbesc($arr['parent_mid']),
+ intval($channel['channel_id'])
+ );
+ }
+ if(! $return) {
+ logger("permission denied for mail delivery {$channel['channel_id']}");
+ $DR->update('permission denied');
+ $result[] = $DR->get();
+ continue;
+ }
+ }
+
+
+ $r = q("select id from mail where mid = '%s' and channel_id = %d limit 1",
+ dbesc($arr['mid']),
+ intval($channel['channel_id'])
+ );
+ if($r) {
+ if(intval($arr['mail_recalled'])) {
+ $x = q("delete from mail where id = %d and channel_id = %d",
+ intval($r[0]['id']),
+ intval($channel['channel_id'])
+ );
+ $DR->update('mail recalled');
+ $result[] = $DR->get();
+ logger('mail_recalled');
+ }
+ else {
+ $DR->update('duplicate mail received');
+ $result[] = $DR->get();
+ logger('duplicate mail received');
+ }
+ continue;
+ }
+ else {
+ $arr['account_id'] = $channel['channel_account_id'];
+ $arr['channel_id'] = $channel['channel_id'];
+ $item_id = mail_store($arr);
+ $DR->update('mail delivered');
+ $result[] = $DR->get();
+ }
+ }
+
+ return $result;
+ }
+
+
+ /**
+ * @brief Processes delivery of profile.
+ *
+ * @see import_directory_profile()
+ * @param array $sender an associative array
+ * * \e string \b hash a xchan_hash
+ * @param array $arr
+ * @param array $deliveries (unused)
+ */
+
+ static function process_profile_delivery($sender, $arr, $deliveries) {
+
+ logger('process_profile_delivery', LOGGER_DEBUG);
+
+ $r = q("select xchan_addr from xchan where xchan_hash = '%s' limit 1",
+ dbesc($sender['hash'])
+ );
+ if($r) {
+ Libzotdir::import_directory_profile($sender, $arr, $r[0]['xchan_addr'], UPDATE_FLAGS_UPDATED, 0);
+ }
+ }
+
+
+ /**
+ * @brief
+ *
+ * @param array $sender an associative array
+ * * \e string \b hash a xchan_hash
+ * @param array $arr
+ * @param array $deliveries (unused) deliveries is irrelevant
+ */
+ static function process_location_delivery($sender, $arr, $deliveries) {
+
+ // deliveries is irrelevant
+ logger('process_location_delivery', LOGGER_DEBUG);
+
+ $r = q("select * from xchan where xchan_hash = '%s' limit 1",
+ dbesc($sender)
+ );
+ if($r) {
+ $xchan = [ 'id' => $r[0]['xchan_guid'], 'id_sig' => $r[0]['xchan_guid_sig'],
+ 'hash' => $r[0]['xchan_hash'], 'public_key' => $r[0]['xchan_pubkey'] ];
+ }
+ if(array_key_exists('locations',$arr) && $arr['locations']) {
+ $x = Libsync::sync_locations($xchan,$arr,true);
+ logger('results: ' . print_r($x,true), LOGGER_DEBUG);
+ if($x['changed']) {
+ $guid = random_string() . '@' . App::get_hostname();
+ Libzotdir::update_modtime($sender,$r[0]['xchan_guid'],$arr['locations'][0]['address'],UPDATE_FLAGS_UPDATED);
+ }
+ }
+ }
+
+ /**
+ * @brief Checks for a moved channel and sets the channel_moved flag.
+ *
+ * Currently the effect of this flag is to turn the channel into 'read-only' mode.
+ * New content will not be processed (there was still an issue with blocking the
+ * ability to post comments as of 10-Mar-2016).
+ * We do not physically remove the channel at this time. The hub admin may choose
+ * to do so, but is encouraged to allow a grace period of several days in case there
+ * are any issues migrating content. This packet will generally be received by the
+ * original site when the basic channel import has been processed.
+ *
+ * This will only be executed on the old location
+ * if a new location is reported and there is only one location record.
+ * The rest of the hubloc syncronisation will be handled within
+ * sync_locations
+ *
+ * @param string $sender_hash A channel hash
+ * @param array $locations
+ */
+
+ static function check_location_move($sender_hash, $locations) {
+
+ if(! $locations)
+ return;
+
+ if(count($locations) != 1)
+ return;
+
+ $loc = $locations[0];
+
+ $r = q("select * from channel where channel_hash = '%s' limit 1",
+ dbesc($sender_hash)
+ );
+
+ if(! $r)
+ return;
+
+ if($loc['url'] !== z_root()) {
+ $x = q("update channel set channel_moved = '%s' where channel_hash = '%s' limit 1",
+ dbesc($loc['url']),
+ dbesc($sender_hash)
+ );
+
+ // federation plugins may wish to notify connections
+ // of the move on singleton networks
+
+ $arr = [
+ 'channel' => $r[0],
+ 'locations' => $locations
+ ];
+ /**
+ * @hooks location_move
+ * Called when a new location has been provided to a UNO channel (indicating a move rather than a clone).
+ * * \e array \b channel
+ * * \e array \b locations
+ */
+ call_hooks('location_move', $arr);
+ }
+ }
+
+
+
+ /**
+ * @brief Returns an array with all known distinct hubs for this channel.
+ *
+ * @see self::get_hublocs()
+ * @param array $channel an associative array which must contain
+ * * \e string \b channel_hash the hash of the channel
+ * @return array an array with associative arrays
+ */
+
+ static function encode_locations($channel) {
+ $ret = [];
+
+ $x = self::get_hublocs($channel['channel_hash']);
+
+ if($x && count($x)) {
+ foreach($x as $hub) {
+
+ // if this is a local channel that has been deleted, the hubloc is no good - make sure it is marked deleted
+ // so that nobody tries to use it.
+
+ if(intval($channel['channel_removed']) && $hub['hubloc_url'] === z_root())
+ $hub['hubloc_deleted'] = 1;
+
+ $ret[] = [
+ 'host' => $hub['hubloc_host'],
+ 'address' => $hub['hubloc_addr'],
+ 'id_url' => $hub['hubloc_id_url'],
+ 'primary' => (intval($hub['hubloc_primary']) ? true : false),
+ 'url' => $hub['hubloc_url'],
+ 'url_sig' => $hub['hubloc_url_sig'],
+ 'site_id' => $hub['hubloc_site_id'],
+ 'callback' => $hub['hubloc_callback'],
+ 'sitekey' => $hub['hubloc_sitekey'],
+ 'deleted' => (intval($hub['hubloc_deleted']) ? true : false)
+ ];
+ }
+ }
+
+ return $ret;
+ }
+
+
+ /**
+ * @brief
+ *
+ * @param array $arr
+ * @param string $pubkey
+ * @return boolean true if updated or inserted
+ */
+
+ static function import_site($arr) {
+
+ if( (! is_array($arr)) || (! $arr['url']) || (! $arr['site_sig']))
+ return false;
+
+ if(! self::verify($arr['url'], $arr['site_sig'], $arr['sitekey'])) {
+ logger('Bad url_sig');
+ return false;
+ }
+
+ $update = false;
+ $exists = false;
+
+ $r = q("select * from site where site_url = '%s' limit 1",
+ dbesc($arr['url'])
+ );
+ if($r) {
+ $exists = true;
+ $siterecord = $r[0];
+ }
+
+ $site_directory = 0;
+ if($arr['directory_mode'] == 'normal')
+ $site_directory = DIRECTORY_MODE_NORMAL;
+ if($arr['directory_mode'] == 'primary')
+ $site_directory = DIRECTORY_MODE_PRIMARY;
+ if($arr['directory_mode'] == 'secondary')
+ $site_directory = DIRECTORY_MODE_SECONDARY;
+ if($arr['directory_mode'] == 'standalone')
+ $site_directory = DIRECTORY_MODE_STANDALONE;
+
+ $register_policy = 0;
+ if($arr['register_policy'] == 'closed')
+ $register_policy = REGISTER_CLOSED;
+ if($arr['register_policy'] == 'open')
+ $register_policy = REGISTER_OPEN;
+ if($arr['register_policy'] == 'approve')
+ $register_policy = REGISTER_APPROVE;
+
+ $access_policy = 0;
+ if(array_key_exists('access_policy',$arr)) {
+ if($arr['access_policy'] === 'private')
+ $access_policy = ACCESS_PRIVATE;
+ if($arr['access_policy'] === 'paid')
+ $access_policy = ACCESS_PAID;
+ if($arr['access_policy'] === 'free')
+ $access_policy = ACCESS_FREE;
+ if($arr['access_policy'] === 'tiered')
+ $access_policy = ACCESS_TIERED;
+ }
+
+ // don't let insecure sites register as public hubs
+
+ if(strpos($arr['url'],'https://') === false)
+ $access_policy = ACCESS_PRIVATE;
+
+ if($access_policy != ACCESS_PRIVATE) {
+ $x = z_fetch_url($arr['url'] . '/siteinfo.json');
+ if(! $x['success'])
+ $access_policy = ACCESS_PRIVATE;
+ }
+
+ $directory_url = htmlspecialchars($arr['directory_url'],ENT_COMPAT,'UTF-8',false);
+ $url = htmlspecialchars(strtolower($arr['url']),ENT_COMPAT,'UTF-8',false);
+ $sellpage = htmlspecialchars($arr['sellpage'],ENT_COMPAT,'UTF-8',false);
+ $site_location = htmlspecialchars($arr['location'],ENT_COMPAT,'UTF-8',false);
+ $site_realm = htmlspecialchars($arr['realm'],ENT_COMPAT,'UTF-8',false);
+ $site_project = htmlspecialchars($arr['project'],ENT_COMPAT,'UTF-8',false);
+ $site_crypto = ((array_key_exists('encryption',$arr) && is_array($arr['encryption'])) ? htmlspecialchars(implode(',',$arr['encryption']),ENT_COMPAT,'UTF-8',false) : '');
+ $site_version = ((array_key_exists('version',$arr)) ? htmlspecialchars($arr['version'],ENT_COMPAT,'UTF-8',false) : '');
+
+ // You can have one and only one primary directory per realm.
+ // Downgrade any others claiming to be primary. As they have
+ // flubbed up this badly already, don't let them be directory servers at all.
+
+ if(($site_directory === DIRECTORY_MODE_PRIMARY)
+ && ($site_realm === get_directory_realm())
+ && ($arr['url'] != get_directory_primary())) {
+ $site_directory = DIRECTORY_MODE_NORMAL;
+ }
+
+ $site_flags = $site_directory;
+
+ if(array_key_exists('zot',$arr)) {
+ set_sconfig($arr['url'],'system','zot_version',$arr['zot']);
+ }
+
+ if($exists) {
+ if(($siterecord['site_flags'] != $site_flags)
+ || ($siterecord['site_access'] != $access_policy)
+ || ($siterecord['site_directory'] != $directory_url)
+ || ($siterecord['site_sellpage'] != $sellpage)
+ || ($siterecord['site_location'] != $site_location)
+ || ($siterecord['site_register'] != $register_policy)
+ || ($siterecord['site_project'] != $site_project)
+ || ($siterecord['site_realm'] != $site_realm)
+ || ($siterecord['site_crypto'] != $site_crypto)
+ || ($siterecord['site_version'] != $site_version) ) {
+
+ $update = true;
+
+ // logger('import_site: input: ' . print_r($arr,true));
+ // logger('import_site: stored: ' . print_r($siterecord,true));
+
+ $r = q("update site set site_dead = 0, site_location = '%s', site_flags = %d, site_access = %d, site_directory = '%s', site_register = %d, site_update = '%s', site_sellpage = '%s', site_realm = '%s', site_type = %d, site_project = '%s', site_version = '%s', site_crypto = '%s'
+ where site_url = '%s'",
+ dbesc($site_location),
+ intval($site_flags),
+ intval($access_policy),
+ dbesc($directory_url),
+ intval($register_policy),
+ dbesc(datetime_convert()),
+ dbesc($sellpage),
+ dbesc($site_realm),
+ intval(SITE_TYPE_ZOT),
+ dbesc($site_project),
+ dbesc($site_version),
+ dbesc($site_crypto),
+ dbesc($url)
+ );
+ if(! $r) {
+ logger('Update failed. ' . print_r($arr,true));
+ }
+ }
+ else {
+ // update the timestamp to indicate we communicated with this site
+ q("update site set site_dead = 0, site_update = '%s' where site_url = '%s'",
+ dbesc(datetime_convert()),
+ dbesc($url)
+ );
+ }
+ }
+ else {
+ $update = true;
+
+ $r = site_store_lowlevel(
+ [
+ 'site_location' => $site_location,
+ 'site_url' => $url,
+ 'site_access' => intval($access_policy),
+ 'site_flags' => intval($site_flags),
+ 'site_update' => datetime_convert(),
+ 'site_directory' => $directory_url,
+ 'site_register' => intval($register_policy),
+ 'site_sellpage' => $sellpage,
+ 'site_realm' => $site_realm,
+ 'site_type' => intval(SITE_TYPE_ZOT),
+ 'site_project' => $site_project,
+ 'site_version' => $site_version,
+ 'site_crypto' => $site_crypto
+ ]
+ );
+
+ if(! $r) {
+ logger('Record create failed. ' . print_r($arr,true));
+ }
+ }
+
+ return $update;
+ }
+
+ /**
+ * @brief Returns path to /rpost
+ *
+ * @todo We probably should make rpost discoverable.
+ *
+ * @param array $observer
+ * * \e string \b xchan_url
+ * @return string
+ */
+ static function get_rpost_path($observer) {
+ if(! $observer)
+ return '';
+
+ $parsed = parse_url($observer['xchan_url']);
+
+ return $parsed['scheme'] . '://' . $parsed['host'] . (($parsed['port']) ? ':' . $parsed['port'] : '') . '/rpost?f=';
+ }
+
+ /**
+ * @brief
+ *
+ * @param array $x
+ * @return boolean|string return false or a hash
+ */
+
+ static function import_author_zot($x) {
+
+ // Check that we have both a hubloc and xchan record - as occasionally storage calls will fail and
+ // we may only end up with one; which results in posts with no author name or photo and are a bit
+ // of a hassle to repair. If either or both are missing, do a full discovery probe.
+
+ $hash = self::make_xchan_hash($x['id'],$x['key']);
+
+ $desturl = $x['url'];
+
+ $r1 = q("select hubloc_url, hubloc_updated, site_dead from hubloc left join site on
+ hubloc_url = site_url where hubloc_guid = '%s' and hubloc_guid_sig = '%s' and hubloc_primary = 1 limit 1",
+ dbesc($x['id']),
+ dbesc($x['id_sig'])
+ );
+
+ $r2 = q("select xchan_hash from xchan where xchan_guid = '%s' and xchan_guid_sig = '%s' limit 1",
+ dbesc($x['id']),
+ dbesc($x['id_sig'])
+ );
+
+ $site_dead = false;
+
+ if($r1 && intval($r1[0]['site_dead'])) {
+ $site_dead = true;
+ }
+
+ // We have valid and somewhat fresh information. Always true if it is our own site.
+
+ if($r1 && $r2 && ( $r1[0]['hubloc_updated'] > datetime_convert('UTC','UTC','now - 1 week') || $r1[0]['hubloc_url'] === z_root() ) ) {
+ logger('in cache', LOGGER_DEBUG);
+ return $hash;
+ }
+
+ logger('not in cache or cache stale - probing: ' . print_r($x,true), LOGGER_DEBUG,LOG_INFO);
+
+ // The primary hub may be dead. Try to find another one associated with this identity that is
+ // still alive. If we find one, use that url for the discovery/refresh probe. Otherwise, the dead site
+ // is all we have and there is no point probing it. Just return the hash indicating we have a
+ // cached entry and the identity is valid. It's just unreachable until they bring back their
+ // server from the grave or create another clone elsewhere.
+
+ if($site_dead) {
+ logger('dead site - ignoring', LOGGER_DEBUG,LOG_INFO);
+
+ $r = q("select hubloc_id_url from hubloc left join site on hubloc_url = site_url
+ where hubloc_hash = '%s' and site_dead = 0",
+ dbesc($hash)
+ );
+ if($r) {
+ logger('found another site that is not dead: ' . $r[0]['hubloc_url'], LOGGER_DEBUG,LOG_INFO);
+ $desturl = $r[0]['hubloc_url'];
+ }
+ else {
+ return $hash;
+ }
+ }
+
+ $them = [ 'hubloc_id_url' => $desturl ];
+ if(self::refresh($them))
+ return $hash;
+
+ return false;
+ }
+
+ static function zotinfo($arr) {
+
+ $ret = [];
+
+ $zhash = ((x($arr,'guid_hash')) ? $arr['guid_hash'] : '');
+ $zguid = ((x($arr,'guid')) ? $arr['guid'] : '');
+ $zguid_sig = ((x($arr,'guid_sig')) ? $arr['guid_sig'] : '');
+ $zaddr = ((x($arr,'address')) ? $arr['address'] : '');
+ $ztarget = ((x($arr,'target_url')) ? $arr['target_url'] : '');
+ $zsig = ((x($arr,'target_sig')) ? $arr['target_sig'] : '');
+ $zkey = ((x($arr,'key')) ? $arr['key'] : '');
+ $mindate = ((x($arr,'mindate')) ? $arr['mindate'] : '');
+ $token = ((x($arr,'token')) ? $arr['token'] : '');
+ $feed = ((x($arr,'feed')) ? intval($arr['feed']) : 0);
+
+ if($ztarget) {
+ $t = q("select * from hubloc where hubloc_id_url = '%s' limit 1",
+ dbesc($ztarget)
+ );
+ if($t) {
+
+ $ztarget_hash = $t[0]['hubloc_hash'];
+
+ }
+ else {
+
+ // should probably perform discovery of the requestor (target) but if they actually had
+ // permissions we would know about them and we only want to know who they are to
+ // enumerate their specific permissions
+
+ $ztarget_hash = EMPTY_STR;
+ }
+ }
+
+
+ $r = null;
+
+ if(strlen($zhash)) {
+ $r = q("select channel.*, xchan.* from channel left join xchan on channel_hash = xchan_hash
+ where channel_hash = '%s' limit 1",
+ dbesc($zhash)
+ );
+ }
+ elseif(strlen($zguid) && strlen($zguid_sig)) {
+ $r = q("select channel.*, xchan.* from channel left join xchan on channel_hash = xchan_hash
+ where channel_guid = '%s' and channel_guid_sig = '%s' limit 1",
+ dbesc($zguid),
+ dbesc($zguid_sig)
+ );
+ }
+ elseif(strlen($zaddr)) {
+ if(strpos($zaddr,'[system]') === false) { /* normal address lookup */
+ $r = q("select channel.*, xchan.* from channel left join xchan on channel_hash = xchan_hash
+ where ( channel_address = '%s' or xchan_addr = '%s' ) limit 1",
+ dbesc($zaddr),
+ dbesc($zaddr)
+ );
+ }
+
+ else {
+
+ /**
+ * The special address '[system]' will return a system channel if one has been defined,
+ * Or the first valid channel we find if there are no system channels.
+ *
+ * This is used by magic-auth if we have no prior communications with this site - and
+ * returns an identity on this site which we can use to create a valid hub record so that
+ * we can exchange signed messages. The precise identity is irrelevant. It's the hub
+ * information that we really need at the other end - and this will return it.
+ *
+ */
+
+ $r = q("select channel.*, xchan.* from channel left join xchan on channel_hash = xchan_hash
+ where channel_system = 1 order by channel_id limit 1");
+ if(! $r) {
+ $r = q("select channel.*, xchan.* from channel left join xchan on channel_hash = xchan_hash
+ where channel_removed = 0 order by channel_id limit 1");
+ }
+ }
+ }
+ else {
+ $ret['message'] = 'Invalid request';
+ return($ret);
+ }
+
+ if(! $r) {
+ $ret['message'] = 'Item not found.';
+ return($ret);
+ }
+
+ $e = $r[0];
+
+ $id = $e['channel_id'];
+
+ $sys_channel = (intval($e['channel_system']) ? true : false);
+ $special_channel = (($e['channel_pageflags'] & PAGE_PREMIUM) ? true : false);
+ $adult_channel = (($e['channel_pageflags'] & PAGE_ADULT) ? true : false);
+ $censored = (($e['channel_pageflags'] & PAGE_CENSORED) ? true : false);
+ $searchable = (($e['channel_pageflags'] & PAGE_HIDDEN) ? false : true);
+ $deleted = (intval($e['xchan_deleted']) ? true : false);
+
+ if($deleted || $censored || $sys_channel)
+ $searchable = false;
+
+ $public_forum = false;
+
+ $role = get_pconfig($e['channel_id'],'system','permissions_role');
+ if($role === 'forum' || $role === 'repository') {
+ $public_forum = true;
+ }
+ else {
+ // check if it has characteristics of a public forum based on custom permissions.
+ $m = \Zotlabs\Access\Permissions::FilledAutoperms($e['channel_id']);
+ if($m) {
+ foreach($m as $k => $v) {
+ if($k == 'tag_deliver' && intval($v) == 1)
+ $ch ++;
+ if($k == 'send_stream' && intval($v) == 0)
+ $ch ++;
+ }
+ if($ch == 2)
+ $public_forum = true;
+ }
+ }
+
+
+ // This is for birthdays and keywords, but must check access permissions
+ $p = q("select * from profile where uid = %d and is_default = 1",
+ intval($e['channel_id'])
+ );
+
+ $profile = array();
+
+ if($p) {
+
+ if(! intval($p[0]['publish']))
+ $searchable = false;
+
+ $profile['description'] = $p[0]['pdesc'];
+ $profile['birthday'] = $p[0]['dob'];
+ if(($profile['birthday'] != '0000-00-00') && (($bd = z_birthday($p[0]['dob'],$e['channel_timezone'])) !== ''))
+ $profile['next_birthday'] = $bd;
+
+ if($age = age($p[0]['dob'],$e['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 = array();
+ $k = explode(' ',$p[0]['keywords']);
+ if($k) {
+ foreach($k as $kk) {
+ if(trim($kk," \t\n\r\0\x0B,")) {
+ $tags[] = trim($kk," \t\n\r\0\x0B,");
+ }
+ }
+ }
+ if($tags)
+ $profile['keywords'] = $tags;
+ }
+ }
+
+ // Communication details
+
+ $ret['id'] = $e['xchan_guid'];
+ $ret['id_sig'] = self::sign($e['xchan_guid'], $e['channel_prvkey']);
+
+ $ret['primary_location'] = [
+ 'address' => $e['xchan_addr'],
+ 'url' => $e['xchan_url'],
+ 'connections_url' => $e['xchan_connurl'],
+ 'follow_url' => $e['xchan_follow'],
+ ];
+
+ $ret['public_key'] = $e['xchan_pubkey'];
+ $ret['username'] = $e['channel_address'];
+ $ret['name'] = $e['xchan_name'];
+ $ret['name_updated'] = $e['xchan_name_date'];
+ $ret['photo'] = [
+ 'url' => $e['xchan_photo_l'],
+ 'type' => $e['xchan_photo_mimetype'],
+ 'updated' => $e['xchan_photo_date']
+ ];
+
+ $ret['channel_role'] = get_pconfig($e['channel_id'],'system','permissions_role','custom');
+
+ $ret['searchable'] = $searchable;
+ $ret['adult_content'] = $adult_channel;
+ $ret['public_forum'] = $public_forum;
+
+ $ret['comments'] = map_scope(\Zotlabs\Access\PermissionLimits::Get($e['channel_id'],'post_comments'));
+ $ret['mail'] = map_scope(\Zotlabs\Access\PermissionLimits::Get($e['channel_id'],'post_mail'));
+
+ if($deleted)
+ $ret['deleted'] = $deleted;
+
+ if(intval($e['channel_removed']))
+ $ret['deleted_locally'] = true;
+
+ // premium or other channel desiring some contact with potential followers before connecting.
+ // This is a template - %s will be replaced with the follow_url we discover for the return channel.
+
+ if($special_channel) {
+ $ret['connect_url'] = (($e['xchan_connpage']) ? $e['xchan_connpage'] : z_root() . '/connect/' . $e['channel_address']);
+ }
+
+ // This is a template for our follow url, %s will be replaced with a webbie
+ if(! $ret['follow_url'])
+ $ret['follow_url'] = z_root() . '/follow?f=&url=%s';
+
+ $permissions = get_all_perms($e['channel_id'],$ztarget_hash,false);
+
+ if($ztarget_hash) {
+ $permissions['connected'] = false;
+ $b = q("select * from abook where abook_xchan = '%s' and abook_channel = %d limit 1",
+ dbesc($ztarget_hash),
+ intval($e['channel_id'])
+ );
+ if($b)
+ $permissions['connected'] = true;
+ }
+
+ if($permissions['view_profile'])
+ $ret['profile'] = $profile;
+
+
+ $concise_perms = [];
+ if($permissions) {
+ foreach($permissions as $k => $v) {
+ if($v) {
+ $concise_perms[] = $k;
+ }
+ }
+ $permissions = implode(',',$concise_perms);
+ }
+
+ $ret['permissions'] = $permissions;
+ $ret['permissions_for'] = $ztarget;
+
+
+ // array of (verified) hubs this channel uses
+
+ $x = self::encode_locations($e);
+ if($x)
+ $ret['locations'] = $x;
+
+ $ret['site'] = self::site_info();
+
+ call_hooks('zotinfo',$ret);
+
+ return($ret);
+
+ }
+
+
+ static function site_info() {
+
+ $signing_key = get_config('system','prvkey');
+ $sig_method = get_config('system','signature_algorithm','sha256');
+
+ $ret = [];
+ $ret['site'] = [];
+ $ret['site']['url'] = z_root();
+ $ret['site']['site_sig'] = self::sign(z_root(), $signing_key);
+ $ret['site']['post'] = z_root() . '/zot';
+ $ret['site']['openWebAuth'] = z_root() . '/owa';
+ $ret['site']['authRedirect'] = z_root() . '/magic';
+ $ret['site']['sitekey'] = get_config('system','pubkey');
+
+ $dirmode = get_config('system','directory_mode');
+ if(($dirmode === false) || ($dirmode == DIRECTORY_MODE_NORMAL))
+ $ret['site']['directory_mode'] = 'normal';
+
+ if($dirmode == DIRECTORY_MODE_PRIMARY)
+ $ret['site']['directory_mode'] = 'primary';
+ elseif($dirmode == DIRECTORY_MODE_SECONDARY)
+ $ret['site']['directory_mode'] = 'secondary';
+ elseif($dirmode == DIRECTORY_MODE_STANDALONE)
+ $ret['site']['directory_mode'] = 'standalone';
+ if($dirmode != DIRECTORY_MODE_NORMAL)
+ $ret['site']['directory_url'] = z_root() . '/dirsearch';
+
+
+ $ret['site']['encryption'] = crypto_methods();
+ $ret['site']['zot'] = System::get_zot_revision();
+
+ // hide detailed site information if you're off the grid
+
+ if($dirmode != DIRECTORY_MODE_STANDALONE) {
+
+ $register_policy = intval(get_config('system','register_policy'));
+
+ if($register_policy == REGISTER_CLOSED)
+ $ret['site']['register_policy'] = 'closed';
+ if($register_policy == REGISTER_APPROVE)
+ $ret['site']['register_policy'] = 'approve';
+ if($register_policy == REGISTER_OPEN)
+ $ret['site']['register_policy'] = 'open';
+
+
+ $access_policy = intval(get_config('system','access_policy'));
+
+ if($access_policy == ACCESS_PRIVATE)
+ $ret['site']['access_policy'] = 'private';
+ if($access_policy == ACCESS_PAID)
+ $ret['site']['access_policy'] = 'paid';
+ if($access_policy == ACCESS_FREE)
+ $ret['site']['access_policy'] = 'free';
+ if($access_policy == ACCESS_TIERED)
+ $ret['site']['access_policy'] = 'tiered';
+
+ $ret['site']['accounts'] = account_total();
+
+ require_once('include/channel.php');
+ $ret['site']['channels'] = channel_total();
+
+ $ret['site']['admin'] = get_config('system','admin_email');
+
+ $visible_plugins = array();
+ if(is_array(\App::$plugins) && count(\App::$plugins)) {
+ $r = q("select * from addon where hidden = 0");
+ if($r)
+ foreach($r as $rr)
+ $visible_plugins[] = $rr['aname'];
+ }
+
+ $ret['site']['plugins'] = $visible_plugins;
+ $ret['site']['sitehash'] = get_config('system','location_hash');
+ $ret['site']['sitename'] = get_config('system','sitename');
+ $ret['site']['sellpage'] = get_config('system','sellpage');
+ $ret['site']['location'] = get_config('system','site_location');
+ $ret['site']['realm'] = get_directory_realm();
+ $ret['site']['project'] = System::get_platform_name();
+ $ret['site']['version'] = System::get_project_version();
+
+ }
+
+ return $ret['site'];
+
+ }
+
+ /**
+ * @brief
+ *
+ * @param array $hub
+ * @param string $sitekey (optional, default empty)
+ *
+ * @return string hubloc_url
+ */
+
+ static function update_hub_connected($hub, $site_id = '') {
+
+ if ($site_id) {
+
+ /*
+ * This hub has now been proven to be valid.
+ * Any hub with the same URL and a different sitekey cannot be valid.
+ * Get rid of them (mark them deleted). There's a good chance they were re-installs.
+ */
+
+ q("update hubloc set hubloc_deleted = 1, hubloc_error = 1 where hubloc_hash = '%s' and hubloc_url = '%s' and hubloc_site_id != '%s' ",
+ dbesc($hub['hubloc_hash']),
+ dbesc($hub['hubloc_url']),
+ dbesc($site_id)
+ );
+
+ }
+ else {
+ $site_id = $hub['hubloc_site_id'];
+ }
+
+ // $sender['sitekey'] is a new addition to the protocol to distinguish
+ // hublocs coming from re-installed sites. Older sites will not provide
+ // this field and we have to still mark them valid, since we can't tell
+ // if this hubloc has the same sitekey as the packet we received.
+ // Update our DB to show when we last communicated successfully with this hub
+ // This will allow us to prune dead hubs from using up resources
+
+ $t = datetime_convert('UTC', 'UTC', 'now - 15 minutes');
+
+ $r = q("update hubloc set hubloc_connected = '%s' where hubloc_id = %d and hubloc_site_id = '%s' and hubloc_connected < '%s' ",
+ dbesc(datetime_convert()),
+ intval($hub['hubloc_id']),
+ dbesc($site_id),
+ dbesc($t)
+ );
+
+ // a dead hub came back to life - reset any tombstones we might have
+
+ if (intval($hub['hubloc_error'])) {
+ q("update hubloc set hubloc_error = 0 where hubloc_id = %d and hubloc_site_id = '%s' ",
+ intval($hub['hubloc_id']),
+ dbesc($site_id)
+ );
+ if (intval($hub['hubloc_orphancheck'])) {
+ q("update hubloc set hubloc_orphancheck = 0 where hubloc_id = %d and hubloc_site_id = '%s' ",
+ intval($hub['hubloc_id']),
+ dbesc($site_id)
+ );
+ }
+ q("update xchan set xchan_orphan = 0 where xchan_orphan = 1 and xchan_hash = '%s'",
+ dbesc($hub['hubloc_hash'])
+ );
+ }
+
+ return $hub['hubloc_url'];
+ }
+
+
+ static function sign($data,$key,$alg = 'sha256') {
+ if(! $key)
+ return 'no key';
+ $sig = '';
+ openssl_sign($data,$sig,$key,$alg);
+ return $alg . '.' . base64url_encode($sig);
+ }
+
+ static function verify($data,$sig,$key) {
+
+ $verify = 0;
+
+ $x = explode('.',$sig,2);
+
+ if ($key && count($x) === 2) {
+ $alg = $x[0];
+ $signature = base64url_decode($x[1]);
+
+ $verify = @openssl_verify($data,$signature,$key,$alg);
+
+ if ($verify === (-1)) {
+ while ($msg = openssl_error_string()) {
+ logger('openssl_verify: ' . $msg,LOGGER_NORMAL,LOG_ERR);
+ }
+ btlogger('openssl_verify: key: ' . $key, LOGGER_DEBUG, LOG_ERR);
+ }
+ }
+ return(($verify > 0) ? true : false);
+ }
+
+
+
+ static function is_zot_request() {
+
+ $x = getBestSupportedMimeType([ 'application/x-zot+json' ]);
+ return(($x) ? true : false);
+ }
+
+}
diff --git a/Zotlabs/Lib/Libzotdir.php b/Zotlabs/Lib/Libzotdir.php
new file mode 100644
index 000000000..91d089c86
--- /dev/null
+++ b/Zotlabs/Lib/Libzotdir.php
@@ -0,0 +1,654 @@
+ $preferred ];
+ }
+ else {
+ return [];
+ }
+ }
+
+
+ /**
+ * 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
+ */
+
+ 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', '');
+ }
+
+
+ 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);
+
+
+ // 'safemode' is the default if there is no observer or no established preference.
+
+ if($setting === 'safemode' && $ret === false)
+ $ret = 1;
+
+ if($setting === 'globaldir' && intval(get_config('system','localdir_hide')))
+ $ret = 1;
+
+ return $ret;
+ }
+
+ /**
+ * @brief Called by the directory_sort widget.
+ */
+ 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, 'pubforums');
+
+ $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['global']);
+ unset($tmp['safe']);
+ unset($tmp['q']);
+ unset($tmp['f']);
+ $forumsurl = $url . http_build_query($tmp) . $suggest;
+
+ $o = replace_macros(get_markup_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('Public Forums Only'),$pubforums,'',array(t('No'), t('Yes')),' onchange=\'window.location.href="' . $forumsurl . '&pubforums="+(this.checked ? 1 : 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)\''),
+ ]);
+
+ return $o;
+ }
+
+ /**
+ * @brief Checks the directory mode of this hub.
+ *
+ * Checks the directory mode of this hub to see if it is some form of directory server. If it is,
+ * get the directory realm of this hub. Fetch a list of all other directory servers in this realm and request
+ * a directory sync packet. This will contain both directory updates and new ratings. Store these all in the DB.
+ * In the case of updates, we will query each of them asynchronously from a poller task. Ratings are stored
+ * directly if the rater's signature matches.
+ *
+ * @param int $dirmode;
+ */
+
+ static function sync_directories($dirmode) {
+
+ if ($dirmode == DIRECTORY_MODE_STANDALONE || $dirmode == DIRECTORY_MODE_NORMAL)
+ return;
+
+ $realm = get_directory_realm();
+ if ($realm == DIRECTORY_REALM) {
+ $r = q("select * from site where (site_flags & %d) > 0 and site_url != '%s' and site_type = %d and ( site_realm = '%s' or site_realm = '') ",
+ intval(DIRECTORY_MODE_PRIMARY|DIRECTORY_MODE_SECONDARY),
+ dbesc(z_root()),
+ intval(SITE_TYPE_ZOT),
+ dbesc($realm)
+ );
+ }
+ else {
+ $r = q("select * from site where (site_flags & %d) > 0 and site_url != '%s' and site_realm like '%s' and site_type = %d ",
+ intval(DIRECTORY_MODE_PRIMARY|DIRECTORY_MODE_SECONDARY),
+ dbesc(z_root()),
+ dbesc(protect_sprintf('%' . $realm . '%')),
+ intval(SITE_TYPE_ZOT)
+ );
+ }
+
+ // If there are no directory servers, setup the fallback master
+ /** @FIXME What to do if we're in a different realm? */
+
+ if ((! $r) && (z_root() != DIRECTORY_FALLBACK_MASTER)) {
+
+ $x = site_store_lowlevel(
+ [
+ 'site_url' => DIRECTORY_FALLBACK_MASTER,
+ 'site_flags' => DIRECTORY_MODE_PRIMARY,
+ 'site_update' => NULL_DATE,
+ 'site_directory' => DIRECTORY_FALLBACK_MASTER . '/dirsearch',
+ 'site_realm' => DIRECTORY_REALM,
+ 'site_valid' => 1,
+ ]
+ );
+
+ $r = q("select * from site where site_flags in (%d, %d) and site_url != '%s' and site_type = %d ",
+ intval(DIRECTORY_MODE_PRIMARY),
+ intval(DIRECTORY_MODE_SECONDARY),
+ dbesc(z_root()),
+ intval(SITE_TYPE_ZOT)
+ );
+ }
+ if (! $r)
+ return;
+
+ foreach ($r as $rr) {
+ if (! $rr['site_directory'])
+ continue;
+
+ logger('sync directories: ' . $rr['site_directory']);
+
+ // for brand new directory servers, only load the last couple of days.
+ // It will take about a month for a new directory to obtain the full current repertoire of channels.
+ /** @FIXME Go back and pick up earlier ratings if this is a new directory server. These do not get refreshed. */
+
+ $token = get_config('system','realm_token');
+
+ $syncdate = (($rr['site_sync'] <= NULL_DATE) ? datetime_convert('UTC','UTC','now - 2 days') : $rr['site_sync']);
+ $x = z_fetch_url($rr['site_directory'] . '?f=&sync=' . urlencode($syncdate) . (($token) ? '&t=' . $token : ''));
+
+ if (! $x['success'])
+ continue;
+
+ $j = json_decode($x['body'],true);
+ if (!($j['transactions']) || ($j['ratings']))
+ continue;
+
+ q("update site set site_sync = '%s' where site_url = '%s'",
+ dbesc(datetime_convert()),
+ dbesc($rr['site_url'])
+ );
+
+ logger('sync_directories: ' . $rr['site_url'] . ': ' . print_r($j,true), LOGGER_DATA);
+
+ if (is_array($j['transactions']) && count($j['transactions'])) {
+ foreach ($j['transactions'] as $t) {
+ $r = q("select * from updates where ud_guid = '%s' limit 1",
+ dbesc($t['transaction_id'])
+ );
+ if($r)
+ continue;
+
+ $ud_flags = 0;
+ if (is_array($t['flags']) && in_array('deleted',$t['flags']))
+ $ud_flags |= UPDATE_FLAGS_DELETED;
+ if (is_array($t['flags']) && in_array('forced',$t['flags']))
+ $ud_flags |= UPDATE_FLAGS_FORCED;
+
+ $z = q("insert into updates ( ud_hash, ud_guid, ud_date, ud_flags, ud_addr )
+ values ( '%s', '%s', '%s', %d, '%s' ) ",
+ dbesc($t['hash']),
+ dbesc($t['transaction_id']),
+ dbesc($t['timestamp']),
+ intval($ud_flags),
+ dbesc($t['address'])
+ );
+ }
+ }
+ }
+ }
+
+
+
+ /**
+ * @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
+ */
+
+ 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 = \Zotlabs\Lib\Webfinger::zot_url(punify($url));
+ if($href) {
+ $zf = \Zotlabs\Lib\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 include/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 boolean $force
+ */
+
+ static function local_dir_update($uid, $force) {
+
+
+ logger('local_dir_update: uid: ' . $uid, LOGGER_DEBUG);
+
+ $p = q("select channel.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 = array();
+ $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 = array();
+ $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 ];
+ call_hooks('local_dir_update', $arr);
+
+ $address = channel_reddress($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_reddress($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 boolean $updated if something changed
+ */
+
+ 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;
+
+ $arr = array();
+
+ $arr['xprof_hash'] = $hash;
+ $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'] = (($profile['age']) ? intval($profile['age']) : 0);
+ $arr['xprof_desc'] = (($profile['description']) ? htmlspecialchars($profile['description'], ENT_COMPAT,'UTF-8',false) : '');
+ $arr['xprof_gender'] = (($profile['gender']) ? htmlspecialchars($profile['gender'], ENT_COMPAT,'UTF-8',false) : '');
+ $arr['xprof_marital'] = (($profile['marital']) ? htmlspecialchars($profile['marital'], ENT_COMPAT,'UTF-8',false) : '');
+ $arr['xprof_sexual'] = (($profile['sexual']) ? htmlspecialchars($profile['sexual'], ENT_COMPAT,'UTF-8',false) : '');
+ $arr['xprof_locale'] = (($profile['locale']) ? htmlspecialchars($profile['locale'], ENT_COMPAT,'UTF-8',false) : '');
+ $arr['xprof_region'] = (($profile['region']) ? htmlspecialchars($profile['region'], ENT_COMPAT,'UTF-8',false) : '');
+ $arr['xprof_postcode'] = (($profile['postcode']) ? htmlspecialchars($profile['postcode'], ENT_COMPAT,'UTF-8',false) : '');
+ $arr['xprof_country'] = (($profile['country']) ? htmlspecialchars($profile['country'], ENT_COMPAT,'UTF-8',false) : '');
+ $arr['xprof_about'] = (($profile['about']) ? htmlspecialchars($profile['about'], ENT_COMPAT,'UTF-8',false) : '');
+ $arr['xprof_homepage'] = (($profile['homepage']) ? htmlspecialchars($profile['homepage'], ENT_COMPAT,'UTF-8',false) : '');
+ $arr['xprof_hometown'] = (($profile['hometown']) ? htmlspecialchars($profile['hometown'], ENT_COMPAT,'UTF-8',false) : '');
+
+ $clean = array();
+ 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'
+ 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_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) values ('%s', '%s', '%s', %d, '%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'])
+ );
+ }
+
+ $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
+ */
+ call_hooks('import_directory_profile', $d);
+
+ if (($d['update']) && (! $suppress_update))
+ self::update_modtime($arr['xprof_hash'],random_string() . '@' . \App::get_hostname(), $addr, $ud_flags);
+
+ return $d['update'];
+ }
+
+ /**
+ * @brief
+ *
+ * @param string $hash An xtag_hash
+ * @param array $keywords
+ */
+
+ static function import_directory_keywords($hash, $keywords) {
+
+ $existing = array();
+ $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 = array();
+ 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
+ */
+
+ 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 not (ud_flags & %d)>0 ",
+ intval(UPDATE_FLAGS_UPDATED),
+ dbesc($addr),
+ intval(UPDATE_FLAGS_UPDATED)
+ );
+ }
+ }
+
+
+
+
+
+
+}
\ No newline at end of file
diff --git a/Zotlabs/Lib/NativeWiki.php b/Zotlabs/Lib/NativeWiki.php
index 6f916216e..cdabbc3e9 100644
--- a/Zotlabs/Lib/NativeWiki.php
+++ b/Zotlabs/Lib/NativeWiki.php
@@ -26,7 +26,8 @@ class NativeWiki {
$w['rawName'] = get_iconfig($w, 'wiki', 'rawName');
$w['htmlName'] = escape_tags($w['rawName']);
- $w['urlName'] = urlencode(urlencode($w['rawName']));
+ //$w['urlName'] = urlencode(urlencode($w['rawName']));
+ $w['urlName'] = self::name_encode($w['rawName']);
$w['mimeType'] = get_iconfig($w, 'wiki', 'mimeType');
$w['typelock'] = get_iconfig($w, 'wiki', 'typelock');
$w['lockstate'] = (($w['allow_cid'] || $w['allow_gid'] || $w['deny_cid'] || $w['deny_gid']) ? 'lock' : 'unlock');
@@ -233,7 +234,8 @@ class NativeWiki {
'wiki' => $w,
'rawName' => $rawName,
'htmlName' => escape_tags($rawName),
- 'urlName' => urlencode(urlencode($rawName)),
+ //'urlName' => urlencode(urlencode($rawName)),
+ 'urlName' => self::name_encode($rawName),
'mimeType' => $mimeType,
'typelock' => $typelock
);
@@ -249,7 +251,8 @@ class NativeWiki {
WHERE resource_type = '%s' AND iconfig.v = '%s' AND uid = %d
AND item_deleted = 0 $sql_extra limit 1",
dbesc(NWIKI_ITEM_RESOURCE_TYPE),
- dbesc(urldecode($urlName)),
+ //dbesc(urldecode($urlName)),
+ dbesc(self::name_decode($urlName)),
intval($uid)
);
@@ -286,4 +289,32 @@ class NativeWiki {
return array('read' => true, 'write' => $write, 'success' => true);
}
}
+
+ public static function name_encode ($string) {
+
+ $string = html_entity_decode($string);
+ $encoding = mb_internal_encoding();
+ mb_internal_encoding("UTF-8");
+ $ret = mb_ereg_replace_callback ('[^A-Za-z0-9\-\_\.\~]',function ($char) {
+ $charhex = unpack('H*',$char[0]);
+ $ret = '('.$charhex[1].')';
+ return $ret;
+ }
+ ,$string);
+ mb_internal_encoding($encoding);
+ return $ret;
+ }
+
+ public static function name_decode ($string) {
+
+ $encoding = mb_internal_encoding();
+ mb_internal_encoding("UTF-8");
+ $ret = mb_ereg_replace_callback ('(\(([0-9a-f]+)\))',function ($chars) {
+ return pack('H*',$chars[2]);
+ }
+ ,$string);
+ mb_internal_encoding($encoding);
+ return $ret;
+ }
+
}
diff --git a/Zotlabs/Lib/NativeWikiPage.php b/Zotlabs/Lib/NativeWikiPage.php
index 919c51276..dddd26af3 100644
--- a/Zotlabs/Lib/NativeWikiPage.php
+++ b/Zotlabs/Lib/NativeWikiPage.php
@@ -44,7 +44,8 @@ class NativeWikiPage {
$pages[] = [
'resource_id' => $resource_id,
'title' => escape_tags($title),
- 'url' => str_replace('%2F','/',urlencode(str_replace('%2F','/',urlencode($title)))),
+ //'url' => str_replace('%2F','/',urlencode(str_replace('%2F','/',urlencode($title)))),
+ 'url' => Zlib\NativeWiki::name_encode($title),
'link_id' => 'id_' . substr($resource_id, 0, 10) . '_' . $page_item['id']
];
}
@@ -98,7 +99,8 @@ class NativeWikiPage {
$page = [
'rawName' => $name,
'htmlName' => escape_tags($name),
- 'urlName' => urlencode($name),
+ //'urlName' => urlencode($name),
+ 'urlName' => Zlib\NativeWiki::name_encode($name)
];
@@ -154,7 +156,8 @@ class NativeWikiPage {
$page = [
'rawName' => $pageNewName,
'htmlName' => escape_tags($pageNewName),
- 'urlName' => urlencode(escape_tags($pageNewName))
+ //'urlName' => urlencode(escape_tags($pageNewName))
+ 'urlName' => Zlib\NativeWiki::name_encode($pageNewName)
];
return [ 'success' => true, 'page' => $page ];
@@ -365,7 +368,6 @@ class NativeWikiPage {
unset($item['id']);
unset($item['author']);
-
$item['parent'] = 0;
$item['body'] = $content;
$item['author_xchan'] = $observer_hash;
@@ -527,7 +529,8 @@ class NativeWikiPage {
$pages = $pageURLs = array();
foreach ($match[1] as $m) {
// TODO: Why do we need to double urlencode for this to work?
- $pageURLs[] = urlencode(urlencode(escape_tags($m)));
+ //$pageURLs[] = urlencode(urlencode(escape_tags($m)));
+ $pageURLs[] = Zlib\NativeWiki::name_encode(escape_tags($m));
$pages[] = $m;
}
$idx = 0;
@@ -556,7 +559,10 @@ class NativeWikiPage {
'$pageHistory' => $pageHistory['history'],
'$permsWrite' => $arr['permsWrite'],
'$name_lbl' => t('Name'),
- '$msg_label' => t('Message','wiki_history')
+ '$msg_label' => t('Message','wiki_history'),
+ '$date_lbl' => t('Date'),
+ '$revert_btn' => t('Revert'),
+ '$compare_btn' => t('Compare')
));
}
@@ -613,7 +619,7 @@ class NativeWikiPage {
$s = str_replace('[observer.webname]', '', $s);
$s = str_replace('[observer.photo]', '', $s);
}
-
+
return $s;
}
diff --git a/Zotlabs/Lib/Queue.php b/Zotlabs/Lib/Queue.php
new file mode 100644
index 000000000..baa1da70d
--- /dev/null
+++ b/Zotlabs/Lib/Queue.php
@@ -0,0 +1,278 @@
+ $base,
+ 'site_update' => datetime_convert(),
+ 'site_dead' => 0,
+ 'site_type' => intval(($outq['outq_driver'] === 'post') ? SITE_TYPE_NOTZOT : SITE_TYPE_UNKNOWN),
+ 'site_crypto' => ''
+ ]
+ );
+ }
+ }
+
+ $arr = array('outq' => $outq, 'base' => $base, 'handled' => false, 'immediate' => $immediate);
+ call_hooks('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 = array();
+ 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;
+ }
+
+ // normal zot delivery
+
+ logger('deliver: dest: ' . $outq['outq_posturl'], LOGGER_DEBUG);
+
+
+ if($outq['outq_posturl'] === z_root() . '/zot') {
+ // local delivery
+ $zot = new \Zotlabs\Zot6\Receiver(new \Zotlabs\Zot6\Zot6Handler(),$outq['outq_notify']);
+ $result = $zot->run(true);
+ 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);
+ }
+ else {
+ logger('remote');
+ $channel = null;
+
+ if($outq['outq_channel']) {
+ $channel = channelx_by_n($outq['outq_channel']);
+ }
+
+ $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' order by hubloc_id desc limit 1",
+ dbesc($base)
+ );
+ if($h) {
+ $host_crypto = $h[0];
+ }
+ }
+
+ $msg = $outq['outq_notify'];
+
+ $result = Libzot::zot($outq['outq_posturl'],$msg,$channel,$host_crypto);
+
+ if($result['success']) {
+ logger('deliver: remote zot delivery succeeded to ' . $outq['outq_posturl']);
+ Libzot::process_response($outq['outq_posturl'],$result, $outq);
+ }
+ else {
+ logger('deliver: remote zot delivery failed to ' . $outq['outq_posturl']);
+ logger('deliver: remote zot delivery fail data: ' . print_r($result,true), LOGGER_DATA);
+ self::update($outq['outq_hash'],10);
+ }
+ }
+ return;
+ }
+}
+
diff --git a/Zotlabs/Lib/ThreadItem.php b/Zotlabs/Lib/ThreadItem.php
index ed78ae00b..78714c2c4 100644
--- a/Zotlabs/Lib/ThreadItem.php
+++ b/Zotlabs/Lib/ThreadItem.php
@@ -2,6 +2,8 @@
namespace Zotlabs\Lib;
+use Zotlabs\Lib\Apps;
+
require_once('include/text.php');
/**
@@ -259,7 +261,7 @@ class ThreadItem {
$forged = ((($item['sig']) && (! intval($item['item_verified']))) ? t('Message signature incorrect') : '');
$unverified = '' ; // (($this->is_wall_to_wall() && (! intval($item['item_verified']))) ? t('Message cannot be verified') : '');
-
+ $settings = '';
// FIXME - check this permission
if($conv->get_profile_owner() == local_channel()) {
@@ -267,12 +269,14 @@ class ThreadItem {
'tagit' => t("Add Tag"),
'classtagger' => "",
);
+
+ $settings = t('Conversation Tools');
}
$has_bookmarks = false;
- if(is_array($item['term'])) {
+ if(Apps::system_app_installed(local_channel(), 'Bookmarks') && is_array($item['term'])) {
foreach($item['term'] as $t) {
- if((get_account_techlevel() > 0) && ($t['ttype'] == TERM_BOOKMARK))
+ if(($t['ttype'] == TERM_BOOKMARK))
$has_bookmarks = true;
}
}
@@ -325,6 +329,10 @@ class ThreadItem {
$has_tags = (($body['tags'] || $body['categories'] || $body['mentions'] || $body['attachments'] || $body['folders']) ? true : false);
+ $dropdown_extras_arr = [ 'item' => $item , 'dropdown_extras' => '' ];
+ call_hooks('dropdown_extras',$dropdown_extras_arr);
+ $dropdown_extras = $dropdown_extras_arr['dropdown_extras'];
+
$tmp_item = array(
'template' => $this->get_template(),
'mode' => $mode,
@@ -404,6 +412,7 @@ class ThreadItem {
'addtocal' => (($has_event) ? t('Add to Calendar') : ''),
'drop' => $drop,
'multidrop' => ((feature_enabled($conv->get_profile_owner(),'multi_delete')) ? $multidrop : ''),
+ 'dropdown_extras' => $dropdown_extras,
// end toolbar buttons
'unseen_comments' => $unseen_comments,
@@ -431,7 +440,8 @@ class ThreadItem {
'preview_lbl' => t('This is an unsaved preview'),
'wait' => t('Please wait'),
'submid' => str_replace(['+','='], ['',''], base64_encode($item['mid'])),
- 'thread_level' => $thread_level
+ 'thread_level' => $thread_level,
+ 'settings' => $settings
);
$arr = array('item' => $item, 'output' => $tmp_item);
diff --git a/Zotlabs/Lib/ThreadStream.php b/Zotlabs/Lib/ThreadStream.php
index d0c964149..020e8729b 100644
--- a/Zotlabs/Lib/ThreadStream.php
+++ b/Zotlabs/Lib/ThreadStream.php
@@ -196,7 +196,6 @@ class ThreadStream {
$item->set_commentable(false);
}
- require_once('include/channel.php');
$item->set_conversation($this);
$this->threads[] = $item;
diff --git a/Zotlabs/Lib/Webfinger.php b/Zotlabs/Lib/Webfinger.php
new file mode 100644
index 000000000..c2364ac4d
--- /dev/null
+++ b/Zotlabs/Lib/Webfinger.php
@@ -0,0 +1,109 @@
+ [ 'Accept: application/jrd+json, */*' ] ]);
+
+ if($s['success']) {
+ $j = json_decode($s['body'], true);
+ return($j);
+ }
+
+ return false;
+ }
+
+ static function parse_resource($resource) {
+
+ self::$resource = urlencode($resource);
+
+ if(strpos($resource,'http') === 0) {
+ $m = parse_url($resource);
+ if($m) {
+ if($m['scheme'] !== 'https') {
+ return false;
+ }
+ self::$server = $m['host'] . (($m['port']) ? ':' . $m['port'] : '');
+ }
+ else {
+ return false;
+ }
+ }
+ elseif(strpos($resource,'tag:') === 0) {
+ $arr = explode(':',$resource); // split the tag
+ $h = explode(',',$arr[1]); // split the host,date
+ self::$server = $h[0];
+ }
+ else {
+ $x = explode('@',$resource);
+ $username = $x[0];
+ if(count($x) > 1) {
+ self::$server = $x[1];
+ }
+ else {
+ return false;
+ }
+ if(strpos($resource,'acct:') !== 0) {
+ self::$resource = urlencode('acct:' . $resource);
+ }
+ }
+
+ }
+
+ /**
+ * @brief fetch a webfinger resource and return a zot6 discovery url if present
+ *
+ */
+
+ static function zot_url($resource) {
+
+ $arr = self::exec($resource);
+
+ if(is_array($arr) && array_key_exists('links',$arr)) {
+ foreach($arr['links'] as $link) {
+ if(array_key_exists('rel',$link) && $link['rel'] === PROTOCOL_ZOT6) {
+ if(array_key_exists('href',$link) && $link['href'] !== EMPTY_STR) {
+ return $link['href'];
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/Zotlabs/Lib/Zotfinger.php b/Zotlabs/Lib/Zotfinger.php
new file mode 100644
index 000000000..537e440d4
--- /dev/null
+++ b/Zotlabs/Lib/Zotfinger.php
@@ -0,0 +1,50 @@
+ 'application/x-zot+json',
+ 'X-Zot-Token' => random_string(),
+ ];
+ $h = HTTPSig::create_sig($headers,$channel['channel_prvkey'],channel_url($channel),false);
+ }
+ else {
+ $h = [ 'Accept: application/x-zot+json' ];
+ }
+
+ $result = [];
+
+
+ $redirects = 0;
+ $x = z_fetch_url($resource,false,$redirects, [ 'headers' => $h ] );
+
+ if($x['success']) {
+
+ $result['signature'] = HTTPSig::verify($x);
+
+ $result['data'] = json_decode($x['body'],true);
+
+ if($result['data'] && is_array($result['data']) && array_key_exists('encrypted',$result['data']) && $result['data']['encrypted']) {
+ $result['data'] = json_decode(crypto_unencapsulate($result['data'],get_config('system','prvkey')),true);
+ }
+
+ return $result;
+ }
+
+ return false;
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/Zotlabs/Module/Acl.php b/Zotlabs/Module/Acl.php
index 0c2ad7522..ea131e08c 100644
--- a/Zotlabs/Module/Acl.php
+++ b/Zotlabs/Module/Acl.php
@@ -81,7 +81,7 @@ class Acl extends \Zotlabs\Web\Controller {
if($search) {
- $sql_extra = " AND groups.gname LIKE " . protect_sprintf( "'%" . dbesc($search) . "%'" ) . " ";
+ $sql_extra = " AND pgrp.gname LIKE " . protect_sprintf( "'%" . dbesc($search) . "%'" ) . " ";
$sql_extra2 = "AND ( xchan_name LIKE " . protect_sprintf( "'%" . dbesc($search) . "%'" ) . " OR xchan_addr LIKE " . protect_sprintf( "'%" . dbesc(punify($search)) . ((strpos($search,'@') === false) ? "%@%'" : "%'")) . ") ";
// This horrible mess is needed because position also returns 0 if nothing is found.
@@ -128,13 +128,13 @@ class Acl extends \Zotlabs\Web\Controller {
// Normal privacy groups
- $r = q("SELECT groups.id, groups.hash, groups.gname
- FROM groups, group_member
- WHERE groups.deleted = 0 AND groups.uid = %d
- AND group_member.gid = groups.id
+ $r = q("SELECT pgrp.id, pgrp.hash, pgrp.gname
+ FROM pgrp, pgrp_member
+ WHERE pgrp.deleted = 0 AND pgrp.uid = %d
+ AND pgrp_member.gid = pgrp.id
$sql_extra
- GROUP BY groups.id
- ORDER BY groups.gname
+ GROUP BY pgrp.id
+ ORDER BY pgrp.gname
LIMIT %d OFFSET %d",
intval(local_channel()),
intval($count),
diff --git a/Zotlabs/Module/Admin.php b/Zotlabs/Module/Admin.php
index 2df8dc25d..6edced9b5 100644
--- a/Zotlabs/Module/Admin.php
+++ b/Zotlabs/Module/Admin.php
@@ -109,7 +109,7 @@ class Admin extends \Zotlabs\Web\Controller {
// available channels, primary and clones
$channels = array();
- $r = q("SELECT COUNT(*) AS total, COUNT(CASE WHEN channel_primary = 1 THEN 1 ELSE NULL END) AS main, COUNT(CASE WHEN channel_primary = 0 THEN 1 ELSE NULL END) AS clones FROM channel WHERE channel_removed = 0");
+ $r = q("SELECT COUNT(*) AS total, COUNT(CASE WHEN channel_primary = 1 THEN 1 ELSE NULL END) AS main, COUNT(CASE WHEN channel_primary = 0 THEN 1 ELSE NULL END) AS clones FROM channel WHERE channel_removed = 0 and channel_system = 0");
if ($r) {
$channels['total'] = array('label' => t('Channels'), 'val' => $r[0]['total']);
$channels['main'] = array('label' => t('Primary'), 'val' => $r[0]['main']);
diff --git a/Zotlabs/Module/Admin/Account_edit.php b/Zotlabs/Module/Admin/Account_edit.php
index 6dfadf183..0300fb10c 100644
--- a/Zotlabs/Module/Admin/Account_edit.php
+++ b/Zotlabs/Module/Admin/Account_edit.php
@@ -31,7 +31,7 @@ class Account_edit {
}
$service_class = trim($_REQUEST['service_class']);
- $account_level = intval(trim($_REQUEST['account_level']));
+ $account_level = 5;
$account_language = trim($_REQUEST['account_language']);
$r = q("update account set account_service_class = '%s', account_level = %d, account_language = '%s'
@@ -68,7 +68,6 @@ class Account_edit {
'$title' => t('Account Edit'),
'$pass1' => [ 'pass1', t('New Password'), ' ','' ],
'$pass2' => [ 'pass2', t('New Password again'), ' ','' ],
- '$account_level' => [ 'account_level', t('Technical skill level'), $x[0]['account_level'], '', \Zotlabs\Lib\Techlevels::levels() ],
'$account_language' => [ 'account_language' , t('Account language (for emails)'), $x[0]['account_language'], '', language_list() ],
'$service_class' => [ 'service_class', t('Service class'), $x[0]['account_service_class'], '' ],
'$submit' => t('Submit'),
@@ -81,4 +80,4 @@ class Account_edit {
}
-}
\ No newline at end of file
+}
diff --git a/Zotlabs/Module/Admin/Site.php b/Zotlabs/Module/Admin/Site.php
index 5912a7c97..09b038729 100644
--- a/Zotlabs/Module/Admin/Site.php
+++ b/Zotlabs/Module/Admin/Site.php
@@ -72,7 +72,6 @@ class Site {
$maxloadavg = ((x($_POST,'maxloadavg')) ? intval(trim($_POST['maxloadavg'])) : 50);
$feed_contacts = ((x($_POST,'feed_contacts')) ? intval($_POST['feed_contacts']) : 0);
$verify_email = ((x($_POST,'verify_email')) ? 1 : 0);
- $techlevel_lock = ((x($_POST,'techlock')) ? intval($_POST['techlock']) : 0);
$imagick_path = ((x($_POST,'imagick_path')) ? trim($_POST['imagick_path']) : '');
$thumbnail_security = ((x($_POST,'thumbnail_security')) ? intval($_POST['thumbnail_security']) : 0);
$force_queue = ((intval($_POST['force_queue']) > 0) ? intval($_POST['force_queue']) : 3000);
@@ -81,10 +80,6 @@ class Site {
$permissions_role = escape_tags(trim($_POST['permissions_role']));
- $techlevel = null;
- if(array_key_exists('techlevel', $_POST))
- $techlevel = intval($_POST['techlevel']);
-
set_config('system', 'feed_contacts', $feed_contacts);
set_config('system', 'delivery_interval', $delivery_interval);
set_config('system', 'delivery_batch_count', $delivery_batch_count);
@@ -110,12 +105,6 @@ class Site {
set_config('system', 'pubstream_incl',$pub_incl);
set_config('system', 'pubstream_excl',$pub_excl);
- set_config('system', 'techlevel_lock', $techlevel_lock);
-
-
-
- if(! is_null($techlevel))
- set_config('system', 'techlevel', $techlevel);
if($directory_server)
set_config('system','directory_server',$directory_server);
@@ -284,15 +273,6 @@ class Site {
// now invert the logic for the setting.
$discover_tab = (1 - $discover_tab);
- $techlevels = [
- '0' => t('Beginner/Basic'),
- '1' => t('Novice - not skilled but willing to learn'),
- '2' => t('Intermediate - somewhat comfortable'),
- '3' => t('Advanced - very comfortable'),
- '4' => t('Expert - I can write computer code'),
- '5' => t('Wizard - I probably know more than you do')
- ];
-
$perm_roles = \Zotlabs\Access\PermissionRoles::roles();
$default_role = get_config('system','default_permissions_role','social');
@@ -316,10 +296,6 @@ class Site {
// name, label, value, help string, extra data...
'$sitename' => array('sitename', t("Site name"), htmlspecialchars(get_config('system','sitename'), ENT_QUOTES, 'UTF-8'),''),
- '$techlevel' => [ 'techlevel', t('Site default technical skill level'), get_config('system','techlevel'), t('Used to provide a member experience matched to technical comfort level'), $techlevels ],
-
- '$techlock' => [ 'techlock', t('Lock the technical skill level setting'), get_config('system','techlevel_lock'), t('Members can set their own technical comfort level by default') ],
-
'$banner' => array('banner', t("Banner/Logo"), $banner, t('Unfiltered HTML/CSS/JS is allowed')),
'$admininfo' => array('admininfo', t("Administrator Information"), $admininfo, t("Contact information for site administrators. Displayed on siteinfo page. BBCode can be used here")),
'$siteinfo' => array('siteinfo', t('Site Information'), get_config('system','siteinfo'), t("Publicly visible description of this site. Displayed on siteinfo page. BBCode can be used here")),
@@ -335,7 +311,7 @@ class Site {
'$access_policy' => array('access_policy', t("Which best describes the types of account offered by this hub?"), get_config('system','access_policy'), t("This is displayed on the public server site list."), $access_choices),
'$register_text' => array('register_text', t("Register text"), htmlspecialchars(get_config('system','register_text'), ENT_QUOTES, 'UTF-8'), t("Will be displayed prominently on the registration page.")),
'$role' => $role,
- '$frontpage' => array('frontpage', t("Site homepage to show visitors (default: login box)"), get_config('system','frontpage'), t("example: 'public' to show public stream, 'page/sys/home' to show a system webpage called 'home' or 'include:home.html' to include a file.")),
+ '$frontpage' => array('frontpage', t("Site homepage to show visitors (default: login box)"), get_config('system','frontpage'), t("example: 'pubstream' to show public stream, 'page/sys/home' to show a system webpage called 'home' or 'include:home.html' to include a file.")),
'$mirror_frontpage' => array('mirror_frontpage', t("Preserve site homepage URL"), get_config('system','mirror_frontpage'), t('Present the site homepage in a frame at the original location instead of redirecting')),
'$abandon_days' => array('abandon_days', t('Accounts abandoned after x days'), get_config('system','account_abandon_days'), t('Will not waste system resources polling external sites for abandonded accounts. Enter 0 for no time limit.')),
'$allowed_sites' => array('allowed_sites', t("Allowed friend domains"), get_config('system','allowed_sites'), t("Comma separated list of domains which are allowed to establish friendships with this site. Wildcards are accepted. Empty to allow any domains")),
diff --git a/Zotlabs/Module/Appman.php b/Zotlabs/Module/Appman.php
index 3ebafafa4..f50dcc2ab 100644
--- a/Zotlabs/Module/Appman.php
+++ b/Zotlabs/Module/Appman.php
@@ -113,10 +113,12 @@ class Appman extends \Zotlabs\Web\Controller {
if($r) {
$app = $r[0];
- $term = q("select * from term where otype = %d and oid = %d",
+ $term = q("select * from term where otype = %d and oid = %d and uid = %d",
intval(TERM_OBJ_APP),
- intval($r[0]['id'])
+ intval($r[0]['id']),
+ intval(local_channel())
);
+
if($term) {
$app['categories'] = '';
foreach($term as $t) {
diff --git a/Zotlabs/Module/Apps.php b/Zotlabs/Module/Apps.php
index 78c8d99ae..05b4495fc 100644
--- a/Zotlabs/Module/Apps.php
+++ b/Zotlabs/Module/Apps.php
@@ -47,11 +47,11 @@ class Apps extends \Zotlabs\Web\Controller {
return replace_macros(get_markup_template('myapps.tpl'), array(
'$sitename' => get_config('system','sitename'),
'$cat' => $cat,
- '$title' => t('Apps'),
+ '$title' => (($available) ? t('Available Apps') : t('Installed Apps')),
'$apps' => $apps,
'$authed' => ((local_channel()) ? true : false),
- '$manage' => (($available) ? '' : t('Manage apps')),
- '$create' => (($mode == 'edit') ? t('Create new app') : '')
+ '$manage' => (($available) ? '' : t('Manage Apps')),
+ '$create' => (($mode == 'edit') ? t('Create Custom App') : '')
));
}
diff --git a/Zotlabs/Module/Article_edit.php b/Zotlabs/Module/Article_edit.php
index 89abccc40..d3cce343f 100644
--- a/Zotlabs/Module/Article_edit.php
+++ b/Zotlabs/Module/Article_edit.php
@@ -122,7 +122,7 @@ class Article_edit extends \Zotlabs\Web\Controller {
'bbcode' => (($mimetype == 'text/bbcode') ? true : false)
);
- $editor = status_editor($a, $x);
+ $editor = status_editor($a, $x, false, 'Article_edit');
$o .= replace_macros(get_markup_template('edpost_head.tpl'), array(
'$title' => t('Edit Article'),
diff --git a/Zotlabs/Module/Articles.php b/Zotlabs/Module/Articles.php
index 284868241..58c16be45 100644
--- a/Zotlabs/Module/Articles.php
+++ b/Zotlabs/Module/Articles.php
@@ -1,12 +1,17 @@
' . t('Articles App') . ' (' . t('Not Installed') . '):
';
+ $o .= t('Create interactive articles');
+ return $o;
}
- nav_set_selected(t('Articles'));
+ nav_set_selected('Articles');
head_add_link([
'rel' => 'alternate',
'type' => 'application/json+oembed',
- 'href' => z_root() . '/oep?f=&url=' . urlencode(z_root() . '/' . \App::$query_string),
+ 'href' => z_root() . '/oep?f=&url=' . urlencode(z_root() . '/' . App::$query_string),
'title' => 'oembed'
]);
@@ -48,19 +58,21 @@ class Articles extends \Zotlabs\Web\Controller {
$category = (($_REQUEST['cat']) ? escape_tags(trim($_REQUEST['cat'])) : '');
if($category) {
- $sql_extra2 .= protect_sprintf(term_item_parent_query(\App::$profile['profile_uid'],'item', $category, TERM_CATEGORY));
+ $sql_extra2 .= protect_sprintf(term_item_parent_query(App::$profile['profile_uid'],'item', $category, TERM_CATEGORY));
}
+ $datequery = ((x($_GET,'dend') && is_a_date_arg($_GET['dend'])) ? notags($_GET['dend']) : '');
+ $datequery2 = ((x($_GET,'dbegin') && is_a_date_arg($_GET['dbegin'])) ? notags($_GET['dbegin']) : '');
$which = argv(1);
$selected_card = ((argc() > 2) ? argv(2) : '');
- $_SESSION['return_url'] = \App::$query_string;
+ $_SESSION['return_url'] = App::$query_string;
$uid = local_channel();
- $owner = \App::$profile_uid;
- $observer = \App::get_observer();
+ $owner = App::$profile_uid;
+ $observer = App::get_observer();
$ob_hash = (($observer) ? $observer['xchan_hash'] : '');
@@ -98,7 +110,7 @@ class Articles extends \Zotlabs\Web\Controller {
'lockstate' => (($channel['channel_allow_cid'] || $channel['channel_allow_gid']
|| $channel['channel_deny_cid'] || $channel['channel_deny_gid']) ? 'lock' : 'unlock'),
'acl' => (($is_owner) ? populate_acl($channel_acl, false,
- \Zotlabs\Lib\PermissionDescription::fromGlobalPermission('view_pages')) : ''),
+ PermissionDescription::fromGlobalPermission('view_pages')) : ''),
'permissions' => $channel_acl,
'showacl' => (($is_owner) ? true : false),
'visitor' => true,
@@ -120,7 +132,7 @@ class Articles extends \Zotlabs\Web\Controller {
$x['title'] = $_REQUEST['title'];
if($_REQUEST['body'])
$x['body'] = $_REQUEST['body'];
- $editor = status_editor($a,$x);
+ $editor = status_editor($a,$x,false,'Articles');
}
else {
@@ -128,8 +140,8 @@ class Articles extends \Zotlabs\Web\Controller {
}
$itemspage = get_pconfig(local_channel(),'system','itemspage');
- \App::set_pager_itemspage(((intval($itemspage)) ? $itemspage : 20));
- $pager_sql = sprintf(" LIMIT %d OFFSET %d ", intval(\App::$pager['itemspage']), intval(\App::$pager['start']));
+ App::set_pager_itemspage(((intval($itemspage)) ? $itemspage : 20));
+ $pager_sql = sprintf(" LIMIT %d OFFSET %d ", intval(App::$pager['itemspage']), intval(App::$pager['start']));
$sql_extra = item_permissions_sql($owner);
@@ -143,10 +155,21 @@ class Articles extends \Zotlabs\Web\Controller {
$sql_item = "and item.id = " . intval($r[0]['iid']) . " ";
}
}
-
+ if($datequery) {
+ $sql_extra2 .= protect_sprintf(sprintf(" AND item.created <= '%s' ", dbesc(datetime_convert(date_default_timezone_get(),'',$datequery))));
+ $order = 'post';
+ }
+ if($datequery2) {
+ $sql_extra2 .= protect_sprintf(sprintf(" AND item.created >= '%s' ", dbesc(datetime_convert(date_default_timezone_get(),'',$datequery2))));
+ }
+
+ if($datequery || $datequery2) {
+ $sql_extra2 .= " and item.item_thread_top != 0 ";
+ }
+
$r = q("select * from item
where item.uid = %d and item_type = %d
- $sql_extra $sql_item order by item.created desc $pager_sql",
+ $sql_extra $sql_extra2 $sql_item order by item.created desc $pager_sql",
intval($owner),
intval(ITEM_TYPE_ARTICLE)
);
@@ -166,7 +189,7 @@ class Articles extends \Zotlabs\Web\Controller {
WHERE item.uid = %d $item_normal
AND item.parent IN ( %s )
$sql_extra $sql_extra2 ",
- intval(\App::$profile['profile_uid']),
+ intval(App::$profile['profile_uid']),
dbesc($parents_str)
);
if($items) {
diff --git a/Zotlabs/Module/Authorize.php b/Zotlabs/Module/Authorize.php
index bfb76150f..c6709f602 100644
--- a/Zotlabs/Module/Authorize.php
+++ b/Zotlabs/Module/Authorize.php
@@ -7,27 +7,34 @@ use Zotlabs\Identity\OAuth2Storage;
class Authorize extends \Zotlabs\Web\Controller {
function get() {
- if (!local_channel()) {
+ if (! local_channel()) {
return login();
- } else {
- // TODO: Fully implement the dynamic client registration protocol:
- // OpenID Connect Dynamic Client Registration 1.0 Client Metadata
- // http://openid.net/specs/openid-connect-registration-1_0.html
- $app = array(
- 'name' => (x($_REQUEST, 'client_name') ? urldecode($_REQUEST['client_name']) : t('Unknown App')),
- 'icon' => (x($_REQUEST, 'logo_uri') ? urldecode($_REQUEST['logo_uri']) : z_root() . '/images/icons/plugin.png'),
- 'url' => (x($_REQUEST, 'client_uri') ? urldecode($_REQUEST['client_uri']) : ''),
- );
- $o .= replace_macros(get_markup_template('oauth_authorize.tpl'), array(
- '$title' => t('Authorize'),
- '$authorize' => sprintf( t('Do you authorize the app %s to access your channel data?'), '' . $app['name'] . ' '),
- '$app' => $app,
- '$yes' => t('Allow'),
- '$no' => t('Deny'),
- '$client_id' => (x($_REQUEST, 'client_id') ? $_REQUEST['client_id'] : ''),
+ }
+ else {
+
+ $name = $_REQUEST['client_name'];
+ if(! $name) {
+ $name = (($_REQUEST['client_id']) ?: t('Unknown App'));
+ }
+
+ $app = [
+ 'name' => $name,
+ 'icon' => (x($_REQUEST, 'logo_uri') ? $_REQUEST['logo_uri'] : z_root() . '/images/icons/plugin.png'),
+ 'url' => (x($_REQUEST, 'client_uri') ? $_REQUEST['client_uri'] : ''),
+ ];
+
+ $link = (($app['url']) ? '' . $app['name'] . ' ' : $app['name']);
+
+ $o .= replace_macros(get_markup_template('oauth_authorize.tpl'), [
+ '$title' => t('Authorize'),
+ '$authorize' => sprintf( t('Do you authorize the app %s to access your channel data?'), $link ),
+ '$app' => $app,
+ '$yes' => t('Allow'),
+ '$no' => t('Deny'),
+ '$client_id' => (x($_REQUEST, 'client_id') ? $_REQUEST['client_id'] : ''),
'$redirect_uri' => (x($_REQUEST, 'redirect_uri') ? $_REQUEST['redirect_uri'] : ''),
- '$state' => (x($_REQUEST, 'state') ? $_REQUEST['state'] : ''),
- ));
+ '$state' => (x($_REQUEST, 'state') ? $_REQUEST['state'] : ''),
+ ]);
return $o;
}
}
@@ -60,13 +67,16 @@ class Authorize extends \Zotlabs\Web\Controller {
$request = \OAuth2\Request::createFromGlobals();
$response = new \OAuth2\Response();
+ // Note, "sub" field must match type and content. $user_id is used to populate - make sure it's a string.
+ $channel = channelx_by_n(local_channel());
+ $user_id = $channel['channel_id'];
+
// If the client is not registered, add to the database
if (!$client = $storage->getClientDetails($client_id)) {
- $client_secret = random_string(16);
+ // Until "Dynamic Client Registration" is pursued - allow new clients to assign their own secret in the REQUEST
+ $client_secret = (isset($_REQUEST['client_secret'])) ? $_REQUEST['client_secret'] : random_string(16);
// Client apps are registered per channel
- $user_id = local_channel();
- $storage->setClientDetails($client_id, $client_secret, $redirect_uri, 'authorization_code', null, $user_id);
-
+ $storage->setClientDetails($client_id, $client_secret, $redirect_uri, 'authorization_code', $_REQUEST['scope'], $user_id);
}
if (!$client = $storage->getClientDetails($client_id)) {
// There was an error registering the client.
@@ -83,7 +93,7 @@ class Authorize extends \Zotlabs\Web\Controller {
// print the authorization code if the user has authorized your client
$is_authorized = ($_POST['authorize'] === 'allow');
- $s->handleAuthorizeRequest($request, $response, $is_authorized, local_channel());
+ $s->handleAuthorizeRequest($request, $response, $is_authorized, $user_id);
if ($is_authorized) {
$code = substr($response->getHttpHeader('Location'), strpos($response->getHttpHeader('Location'), 'code=') + 5, 40);
logger('Authorization Code: ' . $code);
diff --git a/Zotlabs/Module/Blocks.php b/Zotlabs/Module/Blocks.php
index e6a97794d..fde30a6dd 100644
--- a/Zotlabs/Module/Blocks.php
+++ b/Zotlabs/Module/Blocks.php
@@ -109,7 +109,7 @@ class Blocks extends \Zotlabs\Web\Controller {
if($_REQUEST['pagetitle'])
$x['pagetitle'] = $_REQUEST['pagetitle'];
- $editor = status_editor($a,$x);
+ $editor = status_editor($a,$x,false,'Blocks');
$r = q("select iconfig.iid, iconfig.k, iconfig.v, mid, title, body, mimetype, created, edited from iconfig
diff --git a/Zotlabs/Module/Bookmarks.php b/Zotlabs/Module/Bookmarks.php
index e147ffe6c..4b4929c65 100644
--- a/Zotlabs/Module/Bookmarks.php
+++ b/Zotlabs/Module/Bookmarks.php
@@ -1,6 +1,9 @@
' . t('Bookmarks App') . ' (' . t('Not Installed') . '):
';
+ $o .= t('Bookmark links from posts and manage them');
+ return $o;
+ }
require_once('include/menu.php');
require_once('include/conversation.php');
$channel = \App::get_channel();
- //$o = profile_tabs($a,true,$channel['channel_address']);
$o = '';
$o .= '
' . htmlspecialchars(print_array($x)) . ''; + + $headers = 'Accept: application/x-zot+json, application/jrd+json, application/json'; + + $redirects = 0; + $x = z_fetch_url($addr,true,$redirects, [ 'headers' => [ $headers ]]); + + if($x['success']) { + + $o .= '
' . htmlspecialchars($x['header']) . '' . EOL; + + $o .= 'verify returns: ' . str_replace("\n",EOL,print_r(HTTPSig::verify($x),true)) . EOL; + + $o .= '
' . htmlspecialchars(json_encode(json_decode($x['body']),JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)) . '' . EOL; + + } + + } + return $o; + } + +} diff --git a/Zotlabs/Render/Comanche.php b/Zotlabs/Render/Comanche.php index fb400b6fe..cf87cc7d7 100644 --- a/Zotlabs/Render/Comanche.php +++ b/Zotlabs/Render/Comanche.php @@ -441,7 +441,7 @@ class Comanche { $path = 'view/js/jquery.js'; break; case 'bootstrap': - $path = 'library/bootstrap/js/bootstrap.min.js'; + $path = 'vendor/twbs/bootstrap/dist/js/bootstrap.bundle.min.js'; break; case 'foundation': $path = 'library/foundation/js/foundation.js'; @@ -466,7 +466,7 @@ class Comanche { switch($s) { case 'bootstrap': - $path = 'library/bootstrap/css/bootstrap.min.css'; + $path = 'vendor/twbs/bootstrap/dist/css/bootstrap.min.css'; break; case 'foundation': $path = 'library/foundation/css/foundation.min.css'; @@ -528,18 +528,32 @@ class Comanche { $clsname = ucfirst($name); $nsname = "\\Zotlabs\\Widget\\" . $clsname; - if(file_exists('Zotlabs/SiteWidget/' . $clsname . '.php')) - require_once('Zotlabs/SiteWidget/' . $clsname . '.php'); - elseif(file_exists('widget/' . $clsname . '/' . $clsname . '.php')) - require_once('widget/' . $clsname . '/' . $clsname . '.php'); - elseif(file_exists('Zotlabs/Widget/' . $clsname . '.php')) - require_once('Zotlabs/Widget/' . $clsname . '.php'); - else { - $pth = theme_include($clsname . '.php'); - if($pth) { - require_once($pth); + $found = false; + $widgets = \Zotlabs\Extend\Widget::get(); + if($widgets) { + foreach($widgets as $widget) { + if(is_array($widget) && strtolower($widget[1]) === strtolower($name) && file_exists($widget[0])) { + require_once($widget[0]); + $found = true; + } } } + + if(! $found) { + if(file_exists('Zotlabs/SiteWidget/' . $clsname . '.php')) + require_once('Zotlabs/SiteWidget/' . $clsname . '.php'); + elseif(file_exists('widget/' . $clsname . '/' . $clsname . '.php')) + require_once('widget/' . $clsname . '/' . $clsname . '.php'); + elseif(file_exists('Zotlabs/Widget/' . $clsname . '.php')) + require_once('Zotlabs/Widget/' . $clsname . '.php'); + else { + $pth = theme_include($clsname . '.php'); + if($pth) { + require_once($pth); + } + } + } + if(class_exists($nsname)) { $x = new $nsname; $f = 'widget'; diff --git a/Zotlabs/Render/SmartyTemplate.php b/Zotlabs/Render/SmartyTemplate.php index ffe58e286..f14d63064 100755 --- a/Zotlabs/Render/SmartyTemplate.php +++ b/Zotlabs/Render/SmartyTemplate.php @@ -64,17 +64,20 @@ class SmartyTemplate implements TemplateEngine { public function get_intltext_template($file, $root='') { $lang = \App::$language; - - if(file_exists("view/$lang/$file")) - $template_file = "view/$lang/$file"; - elseif(file_exists("view/en/$file")) - $template_file = "view/en/$file"; - else - $template_file = theme_include($file,$root); + if ($root != '' && substr($root,-1) != '/' ) { + $root .= '/'; + } + foreach (Array( + $root."view/$lang/$file", + $root."view/en/$file", + '' + ) as $template_file) { + if (is_file($template_file)) { break; } + } + if ($template_file=='') {$template_file = theme_include($file,$root);} if($template_file) { $template = new SmartyInterface(); $template->filename = $template_file; - return $template; } return ""; diff --git a/Zotlabs/Update/_1217.php b/Zotlabs/Update/_1217.php new file mode 100644 index 000000000..15d2d06b3 --- /dev/null +++ b/Zotlabs/Update/_1217.php @@ -0,0 +1,22 @@ +fetcharr(); $body = $data['body']; + $headers['(request-target)'] = $data['request_target']; } else { @@ -60,6 +61,7 @@ class HTTPSig { strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI']; $headers['content-type'] = $_SERVER['CONTENT_TYPE']; + $headers['content-length'] = $_SERVER['CONTENT_LENGTH']; foreach($_SERVER as $k => $v) { if(strpos($k,'HTTP_') === 0) { @@ -104,6 +106,17 @@ class HTTPSig { if(strpos($h,'.')) { $spoofable = true; } + if($h === 'date') { + $d = new \DateTime($headers[$h]); + $d->setTimeZone(new \DateTimeZone('UTC')); + $dplus = datetime_convert('UTC','UTC','now + 1 day'); + $dminus = datetime_convert('UTC','UTC','now - 1 day'); + $c = $d->format('Y-m-d H:i:s'); + if($c > $dplus || $c < $dminus) { + logger('bad time: ' . $c); + return $result; + } + } } $signed_data = rtrim($signed_data,"\n"); diff --git a/Zotlabs/Web/HttpMeta.php b/Zotlabs/Web/HttpMeta.php index 469a9ed8b..ceaa82162 100644 --- a/Zotlabs/Web/HttpMeta.php +++ b/Zotlabs/Web/HttpMeta.php @@ -54,8 +54,19 @@ class HttpMeta { } } if($this->check_required()) { + $arrayproperties = [ 'og:image' ]; foreach($this->og as $k => $v) { - $o .= '' . "\r\n" ; + if (in_array($k,$arrayproperties)) { + if (is_array($v)) { + foreach ($v as $v2) { + $o .= '' . "\r\n" ; + } + } else { + $o .= '' . "\r\n" ; + } + } else { + $o .= '' . "\r\n" ; + } } } if($o) @@ -63,4 +74,4 @@ class HttpMeta { return $o; } -} \ No newline at end of file +} diff --git a/Zotlabs/Web/Router.php b/Zotlabs/Web/Router.php index fb551e36f..c4db0ef3e 100644 --- a/Zotlabs/Web/Router.php +++ b/Zotlabs/Web/Router.php @@ -2,6 +2,7 @@ namespace Zotlabs\Web; +use Zotlabs\Extend\Route; use Exception; /** @@ -52,14 +53,31 @@ class Router { * First see if we have a plugin which is masquerading as a module. */ - if(is_array(\App::$plugins) && in_array($module,\App::$plugins) && file_exists("addon/{$module}/{$module}.php")) { - include_once("addon/{$module}/{$module}.php"); - if(class_exists($modname)) { - $this->controller = new $modname; - \App::$module_loaded = true; + $routes = Route::get(); + if($routes) { + foreach($routes as $route) { + if(is_array($route) && strtolower($route[1]) === $module) { + include_once($route[0]); + if(class_exists($modname)) { + $this->controller = new $modname; + \App::$module_loaded = true; + } + } } - elseif(function_exists($module . '_module')) { - \App::$module_loaded = true; + } + + // legacy plugins - this can be removed when they have all been converted + + if(! (\App::$module_loaded)) { + if(is_array(\App::$plugins) && in_array($module,\App::$plugins) && file_exists("addon/{$module}/{$module}.php")) { + include_once("addon/{$module}/{$module}.php"); + if(class_exists($modname)) { + $this->controller = new $modname; + \App::$module_loaded = true; + } + elseif(function_exists($module . '_module')) { + \App::$module_loaded = true; + } } } diff --git a/Zotlabs/Web/SubModule.php b/Zotlabs/Web/SubModule.php index 7c8404201..763a55d86 100644 --- a/Zotlabs/Web/SubModule.php +++ b/Zotlabs/Web/SubModule.php @@ -2,6 +2,8 @@ namespace Zotlabs\Web; +use Zotlabs\Extend\Route; + /* * @brief * @@ -31,9 +33,23 @@ class SubModule { $filename = 'Zotlabs/Module/' . ucfirst(argv(0)) . '/'. ucfirst(argv($whicharg)) . '.php'; $modname = '\\Zotlabs\\Module\\' . ucfirst(argv(0)) . '\\' . ucfirst(argv($whicharg)); + if(file_exists($filename)) { $this->controller = new $modname(); } + + $routes = Route::get(); + + if($routes) { + foreach($routes as $route) { + if(is_array($route) && strtolower($route[1]) === strtolower(argv(0)) . '/' . strtolower(argv($whicharg))) { + include_once($route[0]); + if(class_exists($modname)) { + $this->controller = new $modname; + } + } + } + } } /** @@ -43,6 +59,7 @@ class SubModule { * @return boolean|mixed */ function call($method) { + if(! $this->controller) return false; diff --git a/Zotlabs/Widget/Activity_filter.php b/Zotlabs/Widget/Activity_filter.php index fadf39144..4ea0086dd 100644 --- a/Zotlabs/Widget/Activity_filter.php +++ b/Zotlabs/Widget/Activity_filter.php @@ -2,6 +2,8 @@ namespace Zotlabs\Widget; +use Zotlabs\Lib\Apps; + class Activity_filter { function widget($arr) { @@ -44,8 +46,8 @@ class Activity_filter { ]; } - if(feature_enabled(local_channel(),'groups')) { - $groups = q("SELECT * FROM groups WHERE deleted = 0 AND uid = %d ORDER BY gname ASC", + if(Apps::system_app_installed(local_channel(), 'Privacy Groups')) { + $groups = q("SELECT * FROM pgrp WHERE deleted = 0 AND uid = %d ORDER BY gname ASC", intval(local_channel()) ); @@ -180,7 +182,7 @@ class Activity_filter { $arr = ['tabs' => $tabs]; - call_hooks('network_tabs', $arr); + call_hooks('activity_filter', $arr); $o = ''; @@ -190,7 +192,7 @@ class Activity_filter { ]); $o .= replace_macros(get_markup_template('activity_filter_widget.tpl'), [ - '$title' => t('Activity Filters'), + '$title' => t('Stream Filters'), '$reset' => $reset, '$content' => $content, '$name' => $name diff --git a/Zotlabs/Widget/Activity_order.php b/Zotlabs/Widget/Activity_order.php index 7cb08b9e4..1cba1ce8c 100644 --- a/Zotlabs/Widget/Activity_order.php +++ b/Zotlabs/Widget/Activity_order.php @@ -34,6 +34,7 @@ class Activity_order { break; default: $commentord_active = 'active'; + break; } } else { @@ -54,7 +55,7 @@ class Activity_order { } // override order for search, filer and cid results - if(x($_GET,'search') || x($_GET,'file') || (! x($_GET,'pf') && x($_GET,'cid'))) { + if(x($_GET,'search') || x($_GET,'file') || (! x($_GET,'pf') && x($_GET,'cid')) || x($_GET,'verb') || x($_GET,'tag') || x($_GET,'cat')) { $unthreaded_active = 'active'; $commentord_active = $postord_active = 'disabled'; } @@ -109,7 +110,7 @@ class Activity_order { $arr = ['tabs' => $tabs]; - call_hooks('network_tabs', $arr); + call_hooks('activity_order', $arr); $o = ''; @@ -119,7 +120,7 @@ class Activity_order { ]); $o = replace_macros(get_markup_template('common_widget.tpl'), [ - '$title' => t('Activity Order'), + '$title' => t('Stream Order'), '$content' => $content, ]); } diff --git a/Zotlabs/Widget/Appstore.php b/Zotlabs/Widget/Appstore.php index 237707733..6a00ac06a 100644 --- a/Zotlabs/Widget/Appstore.php +++ b/Zotlabs/Widget/Appstore.php @@ -10,9 +10,9 @@ class Appstore { return replace_macros(get_markup_template('appstore.tpl'), [ '$title' => t('App Collections'), '$options' => [ - [ z_root() . '/apps/available', t('Available Apps'), $store ], - [ z_root() . '/apps', t('Installed apps'), 1 - $store ] + [ z_root() . '/apps', t('Installed apps'), 1 - $store ], + [ z_root() . '/apps/available', t('Available Apps'), $store ] ] ]); } -} \ No newline at end of file +} diff --git a/Zotlabs/Widget/Archive.php b/Zotlabs/Widget/Archive.php index c151ca563..9adaac38f 100644 --- a/Zotlabs/Widget/Archive.php +++ b/Zotlabs/Widget/Archive.php @@ -22,12 +22,12 @@ class Archive { return ''; $wall = ((array_key_exists('wall', $arr)) ? intval($arr['wall']) : 0); + $wall = ((array_key_exists('articles', $arr)) ? 2 : $wall); + $style = ((array_key_exists('style', $arr)) ? $arr['style'] : 'select'); $showend = ((get_pconfig($uid,'system','archive_show_end_date')) ? true : false); $mindate = get_pconfig($uid,'system','archive_mindate'); - $visible_years = get_pconfig($uid,'system','archive_visible_years'); - if(! $visible_years) - $visible_years = 5; + $visible_years = get_pconfig($uid,'system','archive_visible_years',5); $url = z_root() . '/' . \App::$cmd; diff --git a/Zotlabs/Widget/Categories.php b/Zotlabs/Widget/Categories.php index 9bfa9742a..27d4b5980 100644 --- a/Zotlabs/Widget/Categories.php +++ b/Zotlabs/Widget/Categories.php @@ -2,6 +2,9 @@ namespace Zotlabs\Widget; +use App; +use Zotlabs\Lib\Apps; + require_once('include/contact_widgets.php'); class Categories { @@ -10,22 +13,22 @@ class Categories { $cards = ((array_key_exists('cards',$arr) && $arr['cards']) ? true : false); - if(($cards) && (! feature_enabled(\App::$profile['profile_uid'],'cards'))) + if(($cards) && (! Apps::system_app_installed(App::$profile['profile_uid'], 'Cards'))) return ''; $articles = ((array_key_exists('articles',$arr) && $arr['articles']) ? true : false); - if(($articles) && (! feature_enabled(\App::$profile['profile_uid'],'articles'))) + if(($articles) && (! feature_enabled(App::$profile['profile_uid'],'articles'))) return ''; - if((! \App::$profile['profile_uid']) - || (! perm_is_allowed(\App::$profile['profile_uid'],get_observer_hash(),(($cards || $articles) ? 'view_pages' : 'view_stream')))) { + if((! App::$profile['profile_uid']) + || (! perm_is_allowed(App::$profile['profile_uid'],get_observer_hash(),(($cards || $articles) ? 'view_pages' : 'view_stream')))) { return ''; } $cat = ((x($_REQUEST,'cat')) ? htmlspecialchars($_REQUEST['cat'],ENT_COMPAT,'UTF-8') : ''); - $srchurl = (($cards) ? \App::$argv[0] . '/' . \App::$argv[1] : \App::$query_string); + $srchurl = (($cards) ? App::$argv[0] . '/' . App::$argv[1] : App::$query_string); $srchurl = rtrim(preg_replace('/cat\=[^\&].*?(\&|$)/is','',$srchurl),'&'); $srchurl = str_replace(array('?f=','&f='),array('',''),$srchurl); diff --git a/Zotlabs/Widget/Cover_photo.php b/Zotlabs/Widget/Cover_photo.php index d2eb1be92..955048992 100644 --- a/Zotlabs/Widget/Cover_photo.php +++ b/Zotlabs/Widget/Cover_photo.php @@ -20,6 +20,16 @@ class Cover_photo { if(! $channel_id) return ''; + // only show cover photos once per login session + $hide_cover = false; + if(array_key_exists('channels_visited',$_SESSION) && is_array($_SESSION['channels_visited']) && in_array($channel_id,$_SESSION['channels_visited'])) { + $hide_cover = true; + } + if(! array_key_exists('channels_visited',$_SESSION)) { + $_SESSION['channels_visited'] = []; + } + $_SESSION['channels_visited'][] = $channel_id; + $channel = channelx_by_n($channel_id); if(array_key_exists('style', $arr) && isset($arr['style'])) @@ -45,6 +55,7 @@ class Cover_photo { $c = get_cover_photo($channel_id,'html'); if($c) { + $c = str_replace('src=', 'data-src=', $c); $photo_html = (($style) ? str_replace('alt=',' style="' . $style . '" alt=',$c) : $c); $o = replace_macros(get_markup_template('cover_photo_widget.tpl'),array( @@ -52,6 +63,7 @@ class Cover_photo { '$title' => $title, '$subtitle' => $subtitle, '$hovertitle' => t('Click to show more'), + '$hide_cover' => $hide_cover )); } return $o; diff --git a/Zotlabs/Widget/Newmember.php b/Zotlabs/Widget/Newmember.php index 1a4b575b9..224f7a8a2 100644 --- a/Zotlabs/Widget/Newmember.php +++ b/Zotlabs/Widget/Newmember.php @@ -17,7 +17,14 @@ class Newmember { if(! $a) return EMPTY_STR; - if(! feature_enabled(local_channel(),'start_menu')) + if($a['account_created'] > datetime_convert('','','now - 60 days')) { + $enabled = get_pconfig(local_channel(), 'system', 'start_menu', 1); + } + else { + $enabled = get_pconfig(local_channel(), 'system', 'start_menu', 0); + } + + if(! $enabled) return EMPTY_STR; $options = [ @@ -44,7 +51,13 @@ class Newmember { t('Miscellaneous'), [ 'settings' => t('Settings'), - 'help' => t('Documentation'), + 'help' => t('Documentation'), + ], + + t('Missing Features?'), + [ + 'apps' => t('Pin apps to navigation bar'), + 'apps/available' => t('Install more apps') ] ]; diff --git a/Zotlabs/Widget/Notes.php b/Zotlabs/Widget/Notes.php index 5c83a550f..238008d81 100644 --- a/Zotlabs/Widget/Notes.php +++ b/Zotlabs/Widget/Notes.php @@ -2,20 +2,26 @@ namespace Zotlabs\Widget; +use Zotlabs\Lib\Apps; + class Notes { function widget($arr) { if(! local_channel()) - return ''; - if(! feature_enabled(local_channel(),'private_notes')) - return ''; + return EMPTY_STR; + + if(! Apps::system_app_installed(local_channel(), 'Notes')) + return EMPTY_STR; $text = get_pconfig(local_channel(),'notes','text'); - $o = replace_macros(get_markup_template('notes.tpl'), array( + $tpl = get_markup_template('notes.tpl'); + + $o = replace_macros($tpl, array( '$banner' => t('Notes'), '$text' => $text, '$save' => t('Save'), + '$app' => ((isset($arr['app'])) ? true : false) )); return $o; diff --git a/Zotlabs/Widget/Notifications.php b/Zotlabs/Widget/Notifications.php index a4cf4e706..0f9f609e4 100644 --- a/Zotlabs/Widget/Notifications.php +++ b/Zotlabs/Widget/Notifications.php @@ -160,7 +160,7 @@ class Notifications { '$notifications' => $notifications, '$no_notifications' => t('Sorry, you have got no notifications at the moment'), '$loading' => t('Loading'), - '$startpage' => get_pconfig(local_channel(), 'system', 'startpage') + '$startpage' => $channel['channel_startpage'] )); return $o; diff --git a/Zotlabs/Widget/Settings_menu.php b/Zotlabs/Widget/Settings_menu.php index f35d6f147..c537c3835 100644 --- a/Zotlabs/Widget/Settings_menu.php +++ b/Zotlabs/Widget/Settings_menu.php @@ -9,15 +9,12 @@ class Settings_menu { if(! local_channel()) return; - $channel = \App::get_channel(); $abook_self_id = 0; // Retrieve the 'self' address book entry for use in the auto-permissions link - $role = get_pconfig(local_channel(),'system','permissions_role'); - $abk = q("select abook_id from abook where abook_channel = %d and abook_self = 1 limit 1", intval(local_channel()) ); @@ -45,19 +42,6 @@ class Settings_menu { ); - if(get_account_techlevel() > 0 && get_features()) { - $tabs[] = array( - 'label' => t('Additional features'), - 'url' => z_root().'/settings/features', - 'selected' => ((argv(1) === 'features') ? 'active' : ''), - ); - } - - $tabs[] = array( - 'label' => t('Addon settings'), - 'url' => z_root().'/settings/featured', - 'selected' => ((argv(1) === 'featured') ? 'active' : ''), - ); $tabs[] = array( 'label' => t('Display settings'), @@ -65,6 +49,12 @@ class Settings_menu { 'selected' => ((argv(1) === 'display') ? 'active' : ''), ); + $tabs[] = array( + 'label' => t('Addon settings'), + 'url' => z_root().'/settings/featured', + 'selected' => ((argv(1) === 'featured') ? 'active' : ''), + ); + if($hublocs) { $tabs[] = array( 'label' => t('Manage locations'), @@ -73,69 +63,6 @@ class Settings_menu { ); } - $tabs[] = array( - 'label' => t('Export channel'), - 'url' => z_root() . '/uexport', - 'selected' => '' - ); - - if(feature_enabled(local_channel(),'oauth_clients')) { - $tabs[] = array( - 'label' => t('OAuth1 apps'), - 'url' => z_root() . '/settings/oauth', - 'selected' => ((argv(1) === 'oauth') ? 'active' : ''), - ); - } - - if(feature_enabled(local_channel(),'oauth2_clients')) { - $tabs[] = array( - 'label' => t('OAuth2 apps'), - 'url' => z_root() . '/settings/oauth2', - 'selected' => ((argv(1) === 'oauth2') ? 'active' : ''), - ); - } - - if(feature_enabled(local_channel(),'access_tokens')) { - $tabs[] = array( - 'label' => t('Guest Access Tokens'), - 'url' => z_root() . '/settings/tokens', - 'selected' => ((argv(1) === 'tokens') ? 'active' : ''), - ); - } - - if(feature_enabled(local_channel(),'permcats')) { - $tabs[] = array( - 'label' => t('Permission Categories'), - 'url' => z_root() . '/settings/permcats', - 'selected' => ((argv(1) === 'permcats') ? 'active' : ''), - ); - } - - - if($role === false || $role === 'custom') { - $tabs[] = array( - 'label' => t('Connection Default Permissions'), - 'url' => z_root() . '/defperms', - 'selected' => '' - ); - } - - if(feature_enabled(local_channel(),'premium_channel')) { - $tabs[] = array( - 'label' => t('Premium Channel Settings'), - 'url' => z_root() . '/connect/' . $channel['channel_address'], - 'selected' => '' - ); - } - - if(feature_enabled(local_channel(),'channel_sources')) { - $tabs[] = array( - 'label' => t('Channel Sources'), - 'url' => z_root() . '/sources', - 'selected' => '' - ); - } - $tabtpl = get_markup_template("generic_links_widget.tpl"); return replace_macros($tabtpl, array( '$title' => t('Settings'), @@ -144,4 +71,4 @@ class Settings_menu { )); } -} \ No newline at end of file +} diff --git a/Zotlabs/Widget/Wiki_list.php b/Zotlabs/Widget/Wiki_list.php index 62f32dbf0..c8d83cbe8 100644 --- a/Zotlabs/Widget/Wiki_list.php +++ b/Zotlabs/Widget/Wiki_list.php @@ -6,13 +6,17 @@ class Wiki_list { function widget($arr) { + if(argc() < 3) { + return; + } + $channel = channelx_by_n(\App::$profile_uid); $wikis = \Zotlabs\Lib\NativeWiki::listwikis($channel,get_observer_hash()); if($wikis) { return replace_macros(get_markup_template('wikilist_widget.tpl'), array( - '$header' => t('Wiki List'), + '$header' => t('Wikis'), '$channel' => $channel['channel_address'], '$wikis' => $wikis['wikis'] )); diff --git a/Zotlabs/Widget/Wiki_page_history.php b/Zotlabs/Widget/Wiki_page_history.php index dcec9a037..dbb322dc3 100644 --- a/Zotlabs/Widget/Wiki_page_history.php +++ b/Zotlabs/Widget/Wiki_page_history.php @@ -20,7 +20,10 @@ class Wiki_page_history { '$pageHistory' => $pageHistory['history'], '$permsWrite' => $arr['permsWrite'], '$name_lbl' => t('Name'), - '$msg_label' => t('Message','wiki_history') + '$msg_label' => t('Message','wiki_history'), + '$date_lbl' => t('Date'), + '$revert_btn' => t('Revert'), + '$compare_btn' => t('Compare') )); } diff --git a/Zotlabs/Widget/Wiki_pages.php b/Zotlabs/Widget/Wiki_pages.php index 06b32b5f5..f178c940d 100644 --- a/Zotlabs/Widget/Wiki_pages.php +++ b/Zotlabs/Widget/Wiki_pages.php @@ -2,6 +2,7 @@ namespace Zotlabs\Widget; +use Zotlabs\Lib\NativeWiki; class Wiki_pages { @@ -10,7 +11,7 @@ class Wiki_pages { return; $c = channelx_by_nick(argv(1)); - $w = \Zotlabs\Lib\NativeWiki::exists_by_name($c['channel_id'],urldecode(argv(2))); + $w = \Zotlabs\Lib\NativeWiki::exists_by_name($c['channel_id'],NativeWiki::name_decode(argv(2))); $arr = array( 'resource_id' => $w['resource_id'], 'channel_id' => $c['channel_id'], @@ -21,8 +22,9 @@ class Wiki_pages { $can_create = perm_is_allowed(\App::$profile['uid'],get_observer_hash(),'write_wiki'); $can_delete = ((local_channel() && (local_channel() == \App::$profile['uid'])) ? true : false); - $pageName = addslashes(escape_tags(urldecode(argv(3)))); + $pageName = NativeWiki::name_decode(escape_tags(argv(3))); + $wikiname = $w['urlName']; return replace_macros(get_markup_template('wiki_page_not_found.tpl'), array( '$resource_id' => $arr['resource_id'], '$channel_address' => $arr['channel_address'], @@ -48,7 +50,7 @@ class Wiki_pages { if(! $arr['resource_id']) { $c = channelx_by_nick(argv(1)); - $w = \Zotlabs\Lib\NativeWiki::exists_by_name($c['channel_id'],urldecode(argv(2))); + $w = \Zotlabs\Lib\NativeWiki::exists_by_name($c['channel_id'],NativeWiki::name_decode(argv(2))); $arr = array( 'resource_id' => $w['resource_id'], 'channel_id' => $c['channel_id'], diff --git a/Zotlabs/Zot/Finger.php b/Zotlabs/Zot/Finger.php index 348171bdc..77634777a 100644 --- a/Zotlabs/Zot/Finger.php +++ b/Zotlabs/Zot/Finger.php @@ -55,7 +55,7 @@ class Finger { $r = q("select xchan.*, hubloc.* from xchan left join hubloc on xchan_hash = hubloc_hash - where xchan_addr = '%s' and hubloc_primary = 1 limit 1", + where xchan_addr = '%s' and hubloc_primary = 1 and hubloc_deleted = 0 limit 1", dbesc($xchan_addr) ); @@ -71,6 +71,11 @@ class Finger { $url = 'https://' . $host; } + $m = parse_url($url); + if($m) { + $parsed_host = strtolower($m['host']); + } + $rhs = '/.well-known/zot-info'; $https = ((strpos($url,'https://') === 0) ? true : false); @@ -88,6 +93,8 @@ class Finger { $headers = []; $headers['X-Zot-Channel'] = $channel['channel_address'] . '@' . \App::get_hostname(); $headers['X-Zot-Nonce'] = random_string(); + $headers['Host'] = $parsed_host; + $xhead = \Zotlabs\Web\HTTPSig::create_sig('',$headers,$channel['channel_prvkey'], 'acct:' . $channel['channel_address'] . '@' . \App::get_hostname(),false); diff --git a/Zotlabs/Zot6/Finger.php b/Zotlabs/Zot6/Finger.php new file mode 100644 index 000000000..f1fe41352 --- /dev/null +++ b/Zotlabs/Zot6/Finger.php @@ -0,0 +1,146 @@ + true) or array('success' => false); + */ + + static public function run($webbie, $channel = null, $autofallback = true) { + + $ret = array('success' => false); + + self::$token = random_string(); + + if (strpos($webbie, '@') === false) { + $address = $webbie; + $host = \App::get_hostname(); + } else { + $address = substr($webbie,0,strpos($webbie,'@')); + $host = substr($webbie,strpos($webbie,'@')+1); + if(strpos($host,'/')) + $host = substr($host,0,strpos($host,'/')); + } + + $xchan_addr = $address . '@' . $host; + + if ((! $address) || (! $xchan_addr)) { + logger('zot_finger: no address :' . $webbie); + + return $ret; + } + + logger('using xchan_addr: ' . $xchan_addr, LOGGER_DATA, LOG_DEBUG); + + // potential issue here; the xchan_addr points to the primary hub. + // The webbie we were called with may not, so it might not be found + // unless we query for hubloc_addr instead of xchan_addr + + $r = q("select xchan.*, hubloc.* from xchan + left join hubloc on xchan_hash = hubloc_hash + where xchan_addr = '%s' and hubloc_primary = 1 limit 1", + dbesc($xchan_addr) + ); + + if($r) { + $url = $r[0]['hubloc_url']; + + if($r[0]['hubloc_network'] && $r[0]['hubloc_network'] !== 'zot') { + logger('zot_finger: alternate network: ' . $webbie); + logger('url: ' . $url . ', net: ' . var_export($r[0]['hubloc_network'],true), LOGGER_DATA, LOG_DEBUG); + return $ret; + } + } else { + $url = 'https://' . $host; + } + + $rhs = '/.well-known/zot-info'; + $https = ((strpos($url,'https://') === 0) ? true : false); + + logger('zot_finger: ' . $address . ' at ' . $url, LOGGER_DEBUG); + + if ($channel) { + $postvars = array( + 'address' => $address, + 'target' => $channel['channel_guid'], + 'target_sig' => $channel['channel_guid_sig'], + 'key' => $channel['channel_pubkey'], + 'token' => self::$token + ); + + $headers = []; + $headers['X-Zot-Channel'] = $channel['channel_address'] . '@' . \App::get_hostname(); + $headers['X-Zot-Nonce'] = random_string(); + $xhead = \Zotlabs\Web\HTTPSig::create_sig('',$headers,$channel['channel_prvkey'], + 'acct:' . $channel['channel_address'] . '@' . \App::get_hostname(),false); + + $retries = 0; + + $result = z_post_url($url . $rhs,$postvars,$retries, [ 'headers' => $xhead ]); + + if ((! $result['success']) && ($autofallback)) { + if ($https) { + logger('zot_finger: https failed. falling back to http'); + $result = z_post_url('http://' . $host . $rhs,$postvars, $retries, [ 'headers' => $xhead ]); + } + } + } + else { + $rhs .= '?f=&address=' . urlencode($address) . '&token=' . self::$token; + + $result = z_fetch_url($url . $rhs); + if((! $result['success']) && ($autofallback)) { + if($https) { + logger('zot_finger: https failed. falling back to http'); + $result = z_fetch_url('http://' . $host . $rhs); + } + } + } + + if(! $result['success']) { + logger('zot_finger: no results'); + + return $ret; + } + + $x = json_decode($result['body'], true); + + $verify = \Zotlabs\Web\HTTPSig::verify($result,(($x) ? $x['key'] : '')); + + if($x && (! $verify['header_valid'])) { + $signed_token = ((is_array($x) && array_key_exists('signed_token', $x)) ? $x['signed_token'] : null); + if($signed_token) { + $valid = zot_verify('token.' . self::$token, base64url_decode($signed_token), $x['key']); + if(! $valid) { + logger('invalid signed token: ' . $url . $rhs, LOGGER_NORMAL, LOG_ERR); + + return $ret; + } + } + else { + logger('No signed token from ' . $url . $rhs, LOGGER_NORMAL, LOG_WARNING); + return $ret; + } + } + + return $x; + } + +} diff --git a/Zotlabs/Zot6/HTTPSig.php b/Zotlabs/Zot6/HTTPSig.php new file mode 100644 index 000000000..a0f0d3500 --- /dev/null +++ b/Zotlabs/Zot6/HTTPSig.php @@ -0,0 +1,507 @@ +fetcharr(); + $body = $data['body']; + } + + else { + $headers = []; + $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI']; + $headers['content-type'] = $_SERVER['CONTENT_TYPE']; + + foreach($_SERVER as $k => $v) { + if(strpos($k,'HTTP_') === 0) { + $field = str_replace('_','-',strtolower(substr($k,5))); + $headers[$field] = $v; + } + } + } + + //logger('SERVER: ' . print_r($_SERVER,true), LOGGER_ALL); + + //logger('headers: ' . print_r($headers,true), LOGGER_ALL); + + return $headers; + } + + + // See draft-cavage-http-signatures-10 + + static function verify($data,$key = '') { + + $body = $data; + $headers = null; + + $result = [ + 'signer' => '', + 'portable_id' => '', + 'header_signed' => false, + 'header_valid' => false, + 'content_signed' => false, + 'content_valid' => false + ]; + + + $headers = self::find_headers($data,$body); + + if(! $headers) + return $result; + + $sig_block = null; + + if(array_key_exists('signature',$headers)) { + $sig_block = self::parse_sigheader($headers['signature']); + } + elseif(array_key_exists('authorization',$headers)) { + $sig_block = self::parse_sigheader($headers['authorization']); + } + + if(! $sig_block) { + logger('no signature provided.', LOGGER_DEBUG); + return $result; + } + + // Warning: This log statement includes binary data + // logger('sig_block: ' . print_r($sig_block,true), LOGGER_DATA); + + $result['header_signed'] = true; + + $signed_headers = $sig_block['headers']; + if(! $signed_headers) + $signed_headers = [ 'date' ]; + + $signed_data = ''; + foreach($signed_headers as $h) { + if(array_key_exists($h,$headers)) { + $signed_data .= $h . ': ' . $headers[$h] . "\n"; + } + } + $signed_data = rtrim($signed_data,"\n"); + + $algorithm = null; + if($sig_block['algorithm'] === 'rsa-sha256') { + $algorithm = 'sha256'; + } + if($sig_block['algorithm'] === 'rsa-sha512') { + $algorithm = 'sha512'; + } + + if(! array_key_exists('keyId',$sig_block)) + return $result; + + $result['signer'] = $sig_block['keyId']; + + $key = self::get_key($key,$result['signer']); + + if(! ($key && $key['public_key'])) { + return $result; + } + + $x = rsa_verify($signed_data,$sig_block['signature'],$key['public_key'],$algorithm); + + logger('verified: ' . $x, LOGGER_DEBUG); + + if(! $x) + return $result; + + $result['portable_id'] = $key['portable_id']; + $result['header_valid'] = true; + + if(in_array('digest',$signed_headers)) { + $result['content_signed'] = true; + $digest = explode('=', $headers['digest'], 2); + if($digest[0] === 'SHA-256') + $hashalg = 'sha256'; + if($digest[0] === 'SHA-512') + $hashalg = 'sha512'; + + if(base64_encode(hash($hashalg,$body,true)) === $digest[1]) { + $result['content_valid'] = true; + } + + logger('Content_Valid: ' . (($result['content_valid']) ? 'true' : 'false')); + } + + return $result; + } + + static function get_key($key,$id) { + + if($key) { + if(function_exists($key)) { + return $key($id); + } + return [ 'public_key' => $key ]; + } + + $key = self::get_webfinger_key($id); + + if(! $key) { + $key = self::get_activitystreams_key($id); + } + + return $key; + + } + + + function convertKey($key) { + + if(strstr($key,'RSA ')) { + return rsatopem($key); + } + elseif(substr($key,0,5) === 'data:') { + return convert_salmon_key($key); + } + else { + return $key; + } + + } + + + /** + * @brief + * + * @param string $id + * @return boolean|string + * false if no pub key found, otherwise return the pub key + */ + + function get_activitystreams_key($id) { + + $x = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_addr = '%s' or hubloc_id_url = '%s' limit 1", + dbesc(str_replace('acct:','',$id)), + dbesc($id) + ); + + if($x && $x[0]['xchan_pubkey']) { + return [ 'portable_id' => $x[0]['xchan_hash'], 'public_key' => $x[0]['xchan_pubkey'] , 'hubloc' => $x[0] ]; + } + + $r = ActivityStreams::fetch_property($id); + + if($r) { + if(array_key_exists('publicKey',$j) && array_key_exists('publicKeyPem',$j['publicKey']) && array_key_exists('id',$j['publicKey'])) { + if($j['publicKey']['id'] === $id || $j['id'] === $id) { + return [ 'public_key' => self::convertKey($j['publicKey']['publicKeyPem']), 'portable_id' => '', 'hubloc' => [] ]; + } + } + } + + return false; + } + + + function get_webfinger_key($id) { + + $x = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_addr = '%s' or hubloc_id_url = '%s' limit 1", + dbesc(str_replace('acct:','',$id)), + dbesc($id) + ); + + if($x && $x[0]['xchan_pubkey']) { + return [ 'portable_id' => $x[0]['xchan_hash'], 'public_key' => $x[0]['xchan_pubkey'] , 'hubloc' => $x[0] ]; + } + + $wf = Webfinger::exec($id); + $key = [ 'portable_id' => '', 'public_key' => '', 'hubloc' => [] ]; + + if($wf) { + if(array_key_exists('properties',$wf) && array_key_exists('https://w3id.org/security/v1#publicKeyPem',$wf['properties'])) { + $key['public_key'] = self::convertKey($wf['properties']['https://w3id.org/security/v1#publicKeyPem']); + } + if(array_key_exists('links', $wf) && is_array($wf['links'])) { + foreach($wf['links'] as $l) { + if(! (is_array($l) && array_key_exists('rel',$l))) { + continue; + } + if($l['rel'] === 'magic-public-key' && array_key_exists('href',$l) && $key['public_key'] === EMPTY_STR) { + $key['public_key'] = self::convertKey($l['href']); + } + } + } + } + + return (($key['public_key']) ? $key : false); + } + + + function get_zotfinger_key($id) { + + $x = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_addr = '%s' or hubloc_id_url = '%s' limit 1", + dbesc(str_replace('acct:','',$id)), + dbesc($id) + ); + if($x && $x[0]['xchan_pubkey']) { + return [ 'portable_id' => $x[0]['xchan_hash'], 'public_key' => $x[0]['xchan_pubkey'] , 'hubloc' => $x[0] ]; + } + + $wf = Webfinger::exec($id); + $key = [ 'portable_id' => '', 'public_key' => '', 'hubloc' => [] ]; + + if($wf) { + if(array_key_exists('properties',$wf) && array_key_exists('https://w3id.org/security/v1#publicKeyPem',$wf['properties'])) { + $key['public_key'] = self::convertKey($wf['properties']['https://w3id.org/security/v1#publicKeyPem']); + } + if(array_key_exists('links', $wf) && is_array($wf['links'])) { + foreach($wf['links'] as $l) { + if(! (is_array($l) && array_key_exists('rel',$l))) { + continue; + } + if($l['rel'] === 'http://purl.org/zot/protocol/6.0' && array_key_exists('href',$l) && $l['href'] !== EMPTY_STR) { + $z = \Zotlabs\Lib\Zotfinger::exec($l['href']); + if($z) { + $i = Zotlabs\Lib\Libzot::import_xchan($z['data']); + if($i['success']) { + $key['portable_id'] = $i['hash']; + + $x = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_id_url = '%s' limit 1", + dbesc($l['href']) + ); + if($x) { + $key['hubloc'] = $x[0]; + } + } + } + } + if($l['rel'] === 'magic-public-key' && array_key_exists('href',$l) && $key['public_key'] === EMPTY_STR) { + $key['public_key'] = self::convertKey($l['href']); + } + } + } + } + + return (($key['public_key']) ? $key : false); + } + + + /** + * @brief + * + * @param array $head + * @param string $prvkey + * @param string $keyid (optional, default '') + * @param boolean $auth (optional, default false) + * @param string $alg (optional, default 'sha256') + * @param array $encryption [ 'key', 'algorithm' ] or false + * @return array + */ + static function create_sig($head, $prvkey, $keyid = EMPTY_STR, $auth = false, $alg = 'sha256', $encryption = false ) { + + $return_headers = []; + + if($alg === 'sha256') { + $algorithm = 'rsa-sha256'; + } + if($alg === 'sha512') { + $algorithm = 'rsa-sha512'; + } + + $x = self::sign($head,$prvkey,$alg); + + $headerval = 'keyId="' . $keyid . '",algorithm="' . $algorithm . '",headers="' . $x['headers'] . '",signature="' . $x['signature'] . '"'; + + if($encryption) { + $x = crypto_encapsulate($headerval,$encryption['key'],$encryption['algorithm']); + if(is_array($x)) { + $headerval = 'iv="' . $x['iv'] . '",key="' . $x['key'] . '",alg="' . $x['alg'] . '",data="' . $x['data'] . '"'; + } + } + + if($auth) { + $sighead = 'Authorization: Signature ' . $headerval; + } + else { + $sighead = 'Signature: ' . $headerval; + } + + if($head) { + foreach($head as $k => $v) { + // strip the request-target virtual header from the output headers + if($k === '(request-target)') { + continue; + } + $return_headers[] = $k . ': ' . $v; + } + } + $return_headers[] = $sighead; + + return $return_headers; + } + + /** + * @brief set headers + * + * @param array $headers + * @return void + */ + + + static function set_headers($headers) { + if($headers && is_array($headers)) { + foreach($headers as $h) { + header($h); + } + } + } + + + /** + * @brief + * + * @param array $head + * @param string $prvkey + * @param string $alg (optional) default 'sha256' + * @return array + */ + + static function sign($head, $prvkey, $alg = 'sha256') { + + $ret = []; + + $headers = ''; + $fields = ''; + + if($head) { + foreach($head as $k => $v) { + $headers .= strtolower($k) . ': ' . trim($v) . "\n"; + if($fields) + $fields .= ' '; + + $fields .= strtolower($k); + } + // strip the trailing linefeed + $headers = rtrim($headers,"\n"); + } + + $sig = base64_encode(rsa_sign($headers,$prvkey,$alg)); + + $ret['headers'] = $fields; + $ret['signature'] = $sig; + + return $ret; + } + + /** + * @brief + * + * @param string $header + * @return array associate array with + * - \e string \b keyID + * - \e string \b algorithm + * - \e array \b headers + * - \e string \b signature + */ + + static function parse_sigheader($header) { + + $ret = []; + $matches = []; + + // if the header is encrypted, decrypt with (default) site private key and continue + + if(preg_match('/iv="(.*?)"/ism',$header,$matches)) + $header = self::decrypt_sigheader($header); + + if(preg_match('/keyId="(.*?)"/ism',$header,$matches)) + $ret['keyId'] = $matches[1]; + if(preg_match('/algorithm="(.*?)"/ism',$header,$matches)) + $ret['algorithm'] = $matches[1]; + if(preg_match('/headers="(.*?)"/ism',$header,$matches)) + $ret['headers'] = explode(' ', $matches[1]); + if(preg_match('/signature="(.*?)"/ism',$header,$matches)) + $ret['signature'] = base64_decode(preg_replace('/\s+/','',$matches[1])); + + if(($ret['signature']) && ($ret['algorithm']) && (! $ret['headers'])) + $ret['headers'] = [ 'date' ]; + + return $ret; + } + + + /** + * @brief + * + * @param string $header + * @param string $prvkey (optional), if not set use site private key + * @return array|string associative array, empty string if failue + * - \e string \b iv + * - \e string \b key + * - \e string \b alg + * - \e string \b data + */ + + static function decrypt_sigheader($header, $prvkey = null) { + + $iv = $key = $alg = $data = null; + + if(! $prvkey) { + $prvkey = get_config('system', 'prvkey'); + } + + $matches = []; + + if(preg_match('/iv="(.*?)"/ism',$header,$matches)) + $iv = $matches[1]; + if(preg_match('/key="(.*?)"/ism',$header,$matches)) + $key = $matches[1]; + if(preg_match('/alg="(.*?)"/ism',$header,$matches)) + $alg = $matches[1]; + if(preg_match('/data="(.*?)"/ism',$header,$matches)) + $data = $matches[1]; + + if($iv && $key && $alg && $data) { + return crypto_unencapsulate([ 'encrypted' => true, 'iv' => $iv, 'key' => $key, 'alg' => $alg, 'data' => $data ] , $prvkey); + } + + return ''; + } + +} diff --git a/Zotlabs/Zot6/IHandler.php b/Zotlabs/Zot6/IHandler.php new file mode 100644 index 000000000..53b6caa89 --- /dev/null +++ b/Zotlabs/Zot6/IHandler.php @@ -0,0 +1,18 @@ +error = false; + $this->validated = false; + $this->messagetype = ''; + $this->response = [ 'success' => false ]; + $this->handler = $handler; + $this->data = null; + $this->rawdata = null; + $this->site_id = null; + $this->prvkey = Config::get('system','prvkey'); + + if($localdata) { + $this->rawdata = $localdata; + } + else { + $this->rawdata = file_get_contents('php://input'); + + // All access to the zot endpoint must use http signatures + + if (! $this->Valid_Httpsig()) { + logger('signature failed'); + $this->error = true; + $this->response['message'] = 'signature invalid'; + return; + } + } + + logger('received raw: ' . print_r($this->rawdata,true), LOGGER_DATA); + + + if ($this->rawdata) { + $this->data = json_decode($this->rawdata,true); + } + else { + $this->error = true; + $this->response['message'] = 'no data'; + } + + logger('received_json: ' . json_encode($this->data,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES), LOGGER_DATA); + + logger('received: ' . print_r($this->data,true), LOGGER_DATA); + + if ($this->data && is_array($this->data)) { + $this->encrypted = ((array_key_exists('encrypted',$this->data) && intval($this->data['encrypted'])) ? true : false); + + if ($this->encrypted && $this->prvkey) { + $uncrypted = crypto_unencapsulate($this->data,$this->prvkey); + if ($uncrypted) { + $this->data = json_decode($uncrypted,true); + } + else { + $this->error = true; + $this->response['message'] = 'no data'; + } + } + } + } + + + function run() { + + if ($this->error) { + // make timing attacks on the decryption engine a bit more difficult + usleep(mt_rand(10000,100000)); + return($this->response); + } + + if ($this->data) { + if (array_key_exists('type',$this->data)) { + $this->messagetype = $this->data['type']; + } + + if (! $this->messagetype) { + $this->error = true; + $this->response['message'] = 'no datatype'; + return $this->response; + } + + $this->sender = ((array_key_exists('sender',$this->data)) ? $this->data['sender'] : null); + $this->recipients = ((array_key_exists('recipients',$this->data)) ? $this->data['recipients'] : null); + $this->site_id = ((array_key_exists('site_id',$this->data)) ? $this->data['site_id'] : null); + } + + if ($this->sender) { + $result = $this->ValidateSender(); + if (! $result) { + $this->error = true; + return $this->response; + } + } + + return $this->Dispatch(); + } + + function ValidateSender() { + + $hub = Libzot::valid_hub($this->sender,$this->site_id); + + if (! $hub) { + $x = Libzot::register_hub($this->sigdata['signer']); + if($x['success']) { + $hub = Libzot::valid_hub($this->sender,$this->site_id); + } + if(! $hub) { + $this->response['message'] = 'sender unknown'; + return false; + } + } + + if (! check_siteallowed($hub['hubloc_url'])) { + $this->response['message'] = 'forbidden'; + return false; + } + + if (! check_channelallowed($this->sender)) { + $this->response['message'] = 'forbidden'; + return false; + } + + Libzot::update_hub_connected($hub,$this->site_id); + + $this->validated = true; + $this->hub = $hub; + return true; + } + + + function Valid_Httpsig() { + + $result = false; + + $this->sigdata = HTTPSig::verify($this->rawdata); + + if ($this->sigdata && $this->sigdata['header_signed'] && $this->sigdata['header_valid']) { + $result = true; + + // It is OK to not have signed content - not all messages provide content. + // But if it is signed, it has to be valid + + if (($this->sigdata['content_signed']) && (! $this->sigdata['content_valid'])) { + $result = false; + } + } + return $result; + } + + function Dispatch() { + + switch ($this->messagetype) { + + case 'request': + $this->response = $this->handler->Request($this->data,$this->hub); + break; + + case 'purge': + $this->response = $this->handler->Purge($this->sender,$this->recipients,$this->hub); + break; + + case 'refresh': + $this->response = $this->handler->Refresh($this->sender,$this->recipients,$this->hub); + break; + + case 'rekey': + $this->response = $this->handler->Rekey($this->sender, $this->data,$this->hub); + break; + + case 'activity': + case 'response': // upstream message + case 'sync': + default: + $this->response = $this->handler->Notify($this->data,$this->hub); + break; + + } + + logger('response_to_return: ' . print_r($this->response,true),LOGGER_DATA); + + if ($this->encrypted) { + $this->EncryptResponse(); + } + + return($this->response); + } + + function EncryptResponse() { + $algorithm = Libzot::best_algorithm($this->hub['site_crypto']); + if ($algorithm) { + $this->response = crypto_encapsulate(json_encode($this->response),$this->hub['hubloc_sitekey'], $algorithm); + } + } + +} + + + diff --git a/Zotlabs/Zot6/Zot6Handler.php b/Zotlabs/Zot6/Zot6Handler.php new file mode 100644 index 000000000..5597921cc --- /dev/null +++ b/Zotlabs/Zot6/Zot6Handler.php @@ -0,0 +1,266 @@ + false ]; + + logger('notify received from ' . $hub['hubloc_url']); + + $x = Libzot::fetch($data); + $ret['delivery_report'] = $x; + + + $ret['success'] = true; + return $ret; + } + + + + /** + * @brief Remote channel info (such as permissions or photo or something) + * has been updated. Grab a fresh copy and sync it. + * + * The difference between refresh and force_refresh is that force_refresh + * unconditionally creates a directory update record, even if no changes were + * detected upon processing. + * + * @param array $sender + * @param array $recipients + * + * @return json_return_and_die() + */ + + static function reply_refresh($sender, $recipients,$hub) { + $ret = array('success' => false); + + if($recipients) { + + // This would be a permissions update, typically for one connection + + foreach ($recipients as $recip) { + $r = q("select channel.*,xchan.* from channel + left join xchan on channel_hash = xchan_hash + where channel_hash ='%s' limit 1", + dbesc($recip) + ); + + $x = Libzot::refresh( [ 'hubloc_id_url' => $hub['hubloc_id_url'] ], $r[0], (($msgtype === 'force_refresh') ? true : false)); + } + } + else { + // system wide refresh + + $x = Libzot::refresh( [ 'hubloc_id_url' => $hub['hubloc_id_url'] ], null, (($msgtype === 'force_refresh') ? true : false)); + } + + $ret['success'] = true; + return $ret; + } + + + + /** + * @brief Process a message request. + * + * If a site receives a comment to a post but finds they have no parent to attach it with, they + * may send a 'request' packet containing the message_id of the missing parent. This is the handler + * for that packet. We will create a message_list array of the entire conversation starting with + * the missing parent and invoke delivery to the sender of the packet. + * + * Zotlabs/Daemon/Deliver.php (for local delivery) and + * mod/post.php???? @fixme (for web delivery) detect the existence of + * this 'message_list' at the destination and split it into individual messages which are + * processed/delivered in order. + * + * + * @param array $data + * @return array + */ + + static function reply_message_request($data,$hub) { + $ret = [ 'success' => false ]; + + $message_id = EMPTY_STR; + + if(array_key_exists('data',$data)) + $ptr = $data['data']; + if(is_array($ptr) && array_key_exists(0,$ptr)) { + $ptr = $ptr[0]; + } + if(is_string($ptr)) { + $message_id = $ptr; + } + if(is_array($ptr) && array_key_exists('id',$ptr)) { + $message_id = $ptr['id']; + } + + if (! $message_id) { + $ret['message'] = 'no message_id'; + logger('no message_id'); + return $ret; + } + + $sender = $hub['hubloc_hash']; + + /* + * Find the local channel in charge of this post (the first and only recipient of the request packet) + */ + + $arr = $data['recipients'][0]; + + $c = q("select * from channel left join xchan on channel_hash = xchan_hash where channel_hash = '%s' limit 1", + dbesc($arr['portable_id']) + ); + if (! $c) { + logger('recipient channel not found.'); + $ret['message'] .= 'recipient not found.' . EOL; + return $ret; + } + + /* + * fetch the requested conversation + */ + + $messages = zot_feed($c[0]['channel_id'],$sender_hash, [ 'message_id' => $data['message_id'], 'encoding' => 'activitystreams' ]); + + return (($messages) ? : [] ); + + } + + static function rekey_request($sender,$data,$hub) { + + $ret = array('success' => false); + + // newsig is newkey signed with oldkey + + // The original xchan will remain. In Zot/Receiver we will have imported the new xchan and hubloc to verify + // the packet authenticity. What we will do now is verify that the keychange operation was signed by the + // oldkey, and if so change all the abook, abconfig, group, and permission elements which reference the + // old xchan_hash. + + if((! $data['old_key']) && (! $data['new_key']) && (! $data['new_sig'])) + return $ret; + + + $old = null; + + if(Libzot::verify($data['old_guid'],$data['old_guid_sig'],$data['old_key'])) { + $oldhash = make_xchan_hash($data['old_guid'],$data['old_key']); + $old = q("select * from xchan where xchan_hash = '%s' limit 1", + dbesc($oldhash) + ); + } + else + return $ret; + + + if(! $old) { + return $ret; + } + + $xchan = $old[0]; + + if(! Libzot::verify($data['new_key'],$data['new_sig'],$xchan['xchan_pubkey'])) { + return $ret; + } + + $r = q("select * from xchan where xchan_hash = '%s' limit 1", + dbesc($sender) + ); + + $newxchan = $r[0]; + + // @todo + // if ! $update create a linked identity + + + xchan_change_key($xchan,$newxchan,$data); + + $ret['success'] = true; + return $ret; + } + + + /** + * @brief + * + * @param array $sender + * @param array $recipients + * + * return json_return_and_die() + */ + + static function reply_purge($sender, $recipients, $hub) { + + $ret = array('success' => false); + + if ($recipients) { + // basically this means "unfriend" + foreach ($recipients as $recip) { + $r = q("select channel.*,xchan.* from channel + left join xchan on channel_hash = xchan_hash + where channel_hash = '%s' and channel_guid_sig = '%s' limit 1", + dbesc($recip) + ); + if ($r) { + $r = q("select abook_id from abook where uid = %d and abook_xchan = '%s' limit 1", + intval($r[0]['channel_id']), + dbesc($sender) + ); + if ($r) { + contact_remove($r[0]['channel_id'],$r[0]['abook_id']); + } + } + } + $ret['success'] = true; + } + else { + + // Unfriend everybody - basically this means the channel has committed suicide + + remove_all_xchan_resources($sender); + + $ret['success'] = true; + } + + return $ret; + } + + + + + + +} diff --git a/app/admin.apd b/app/admin.apd deleted file mode 100644 index 68c07568e..000000000 --- a/app/admin.apd +++ /dev/null @@ -1,6 +0,0 @@ -version: 1 -url: $baseurl/admin -requires: admin -name: Admin -photo: icon:wrench -categories: nav_featured_app diff --git a/app/articles.apd b/app/articles.apd index 5a9f17e0f..525d0443e 100644 --- a/app/articles.apd +++ b/app/articles.apd @@ -1,6 +1,6 @@ -version: 1.2 +version: 2 url: $baseurl/articles/$nick name: Articles -requires: local_channel, articles +requires: local_channel photo: icon:file-text-o categories: nav_featured_app, Productivity diff --git a/app/bookmarks.apd b/app/bookmarks.apd index 33d663217..fc1b68d50 100644 --- a/app/bookmarks.apd +++ b/app/bookmarks.apd @@ -1,6 +1,6 @@ -version: 1 +version: 2 url: $baseurl/bookmarks requires: local_channel -name: View Bookmarks +name: Bookmarks photo: icon:bookmark categories: Productivity diff --git a/app/caldav.apd b/app/caldav.apd index a5839fd6e..30b45b4bd 100644 --- a/app/caldav.apd +++ b/app/caldav.apd @@ -1,5 +1,5 @@ -version: 1 -url: $baseurl/cdav/calendar +version: 2 +url: $baseurl/cdav/calendar, $baseurl/settings/calendar requires: local_channel name: CalDAV photo: icon:calendar diff --git a/app/carddav.apd b/app/carddav.apd index 3b60ebcfe..d32e636b4 100644 --- a/app/carddav.apd +++ b/app/carddav.apd @@ -1,4 +1,4 @@ -version: 1 +version: 2 url: $baseurl/cdav/addressbook requires: local_channel name: CardDAV diff --git a/app/cards.apd b/app/cards.apd index 8e2762ff8..780f71e75 100644 --- a/app/cards.apd +++ b/app/cards.apd @@ -1,6 +1,6 @@ -version: 1.1 +version: 2 url: $baseurl/cards/$nick name: Cards -requires: local_channel, cards +requires: local_channel photo: icon:list categories: nav_featured_app, Productivity diff --git a/app/channel.apd b/app/channel.apd index ab79e7047..d48266c95 100644 --- a/app/channel.apd +++ b/app/channel.apd @@ -1,5 +1,5 @@ -version: 1 -url: $baseurl/channel/$nick +version: 2 +url: $baseurl/channel/$nick, $baseurl/settings/channel_home requires: local_channel name: Channel Home photo: icon:home diff --git a/app/chat.apd b/app/chat.apd index b59d846a6..627c62d12 100644 --- a/app/chat.apd +++ b/app/chat.apd @@ -1,6 +1,6 @@ -version: 1 +version: 2 url: $baseurl/chat/$nick -requires: local_channel, ajaxchat -name: My Chatrooms +requires: local_channel +name: Chatrooms photo: icon:comments-o categories: Productivity diff --git a/app/connections.apd b/app/connections.apd index 631f093a8..0e4c7d670 100644 --- a/app/connections.apd +++ b/app/connections.apd @@ -1,5 +1,5 @@ -version: 1 -url: $baseurl/connections +version: 2 +url: $baseurl/connections, $baseurl/settings/connections requires: local_channel name: Connections photo: icon:users diff --git a/app/defperm.apd b/app/defperm.apd new file mode 100644 index 000000000..2b241ee7f --- /dev/null +++ b/app/defperm.apd @@ -0,0 +1,6 @@ +version: 2 +url: $baseurl/defperms +requires: local_channel, custom_role +name: Default Permissions +photo: icon:unlock-alt +categories: Access Control diff --git a/app/directory.apd b/app/directory.apd index 03a93f042..887c31e30 100644 --- a/app/directory.apd +++ b/app/directory.apd @@ -1,5 +1,5 @@ -version: 1 -url: $baseurl/directory +version: 2 +url: $baseurl/directory, $baseurl/settings/directory name: Directory photo: icon:sitemap categories: nav_featured_app, Networking diff --git a/app/events.apd b/app/events.apd index dc3257d77..b69ee35ee 100644 --- a/app/events.apd +++ b/app/events.apd @@ -1,5 +1,5 @@ -version: 1 -url: $baseurl/events +version: 2 +url: $baseurl/events, $baseurl/settings/events requires: local_channel name: Events photo: icon:calendar diff --git a/app/group.apd b/app/group.apd index d16b9237c..da0b31407 100644 --- a/app/group.apd +++ b/app/group.apd @@ -1,4 +1,4 @@ -version: 1 +version: 2 url: $baseurl/group requires: local_channel name: Privacy Groups diff --git a/app/help.apd b/app/help.apd index c81584178..a0e6a491b 100644 --- a/app/help.apd +++ b/app/help.apd @@ -1,4 +1,4 @@ -version: 1 +version: 2 url: $baseurl/help name: Help photo: icon:question diff --git a/app/invite.apd b/app/invite.apd index 99c2a4eec..5c0e8d09f 100644 --- a/app/invite.apd +++ b/app/invite.apd @@ -1,4 +1,4 @@ -version: 1 +version: 2 url: $baseurl/invite requires: local_channel name: Invite diff --git a/app/lang.apd b/app/lang.apd index c30a74654..65495dd5b 100644 --- a/app/lang.apd +++ b/app/lang.apd @@ -1,4 +1,4 @@ -version: 1 +version: 2 url: $baseurl/lang name: Language photo: icon:language diff --git a/app/mail.apd b/app/mail.apd index 9df9139f3..f94a2b3c7 100644 --- a/app/mail.apd +++ b/app/mail.apd @@ -1,4 +1,4 @@ -version: 1 +version: 2 url: $baseurl/mail/combined requires: local_channel name: Mail diff --git a/app/mood.apd b/app/mood.apd index e05606a0e..dd4e51cec 100644 --- a/app/mood.apd +++ b/app/mood.apd @@ -1,4 +1,4 @@ -version: 1 +version: 2 url: $baseurl/mood requires: local_channel name: Mood diff --git a/app/grid.apd b/app/network.apd similarity index 51% rename from app/grid.apd rename to app/network.apd index c826974a4..f67b48ffe 100644 --- a/app/grid.apd +++ b/app/network.apd @@ -1,6 +1,6 @@ -version: 1 -url: $baseurl/network +version: 2 +url: $baseurl/network, $baseurl/settings/network requires: local_channel -name: Grid +name: Network photo: icon:th categories: nav_featured_app, Networking diff --git a/app/notes.apd b/app/notes.apd new file mode 100644 index 000000000..01f94c60a --- /dev/null +++ b/app/notes.apd @@ -0,0 +1,6 @@ +version: 2 +url: $baseurl/notes +requires: local_channel +name: Notes +photo: icon:sticky-note-o +categories: Personal, Productivity diff --git a/app/oauth.apd b/app/oauth.apd new file mode 100644 index 000000000..5e69e4401 --- /dev/null +++ b/app/oauth.apd @@ -0,0 +1,6 @@ +version: 2 +url: $baseurl/oauth +requires: local_channel +name: OAuth Apps Manager +photo: icon:chevron-circle-up +categories: Access Control diff --git a/app/oauth2.apd b/app/oauth2.apd new file mode 100644 index 000000000..86828bf24 --- /dev/null +++ b/app/oauth2.apd @@ -0,0 +1,6 @@ +version: 2 +url: $baseurl/oauth2 +requires: local_channel +name: OAuth2 Apps Manager +photo: icon:chevron-circle-up +categories: Access Control diff --git a/app/pdledit.apd b/app/pdledit.apd new file mode 100644 index 000000000..fbc643296 --- /dev/null +++ b/app/pdledit.apd @@ -0,0 +1,6 @@ +version: 2 +url: $baseurl/pdledit +requires: local_channel +name: PDL Editor +photo: icon:object-group +categories: Appearance diff --git a/app/permcats.apd b/app/permcats.apd new file mode 100644 index 000000000..e9fd6e56f --- /dev/null +++ b/app/permcats.apd @@ -0,0 +1,6 @@ +version: 2 +url: $baseurl/permcats +requires: local_channel +name: Permission Categories +photo: icon:unlock-alt +categories: Access Control diff --git a/app/photos.apd b/app/photos.apd index 048e623bb..b28b315cd 100644 --- a/app/photos.apd +++ b/app/photos.apd @@ -1,5 +1,5 @@ -version: 1 -url: $baseurl/photos/$nick +version: 2 +url: $baseurl/photos/$nick, $baseurl/settings/photos requires: local_channel name: Photos photo: icon:photo diff --git a/app/poke.apd b/app/poke.apd index 37b640fb8..cf23c29b4 100644 --- a/app/poke.apd +++ b/app/poke.apd @@ -1,4 +1,4 @@ -version: 1 +version: 2 url: $baseurl/poke requires: local_channel name: Poke diff --git a/app/post.apd b/app/post.apd index 08e5ccf0c..d3ce88454 100644 --- a/app/post.apd +++ b/app/post.apd @@ -1,4 +1,4 @@ -version: 1 +version: 2 url: $baseurl/rpost?f=&body= requires: observer name: Post diff --git a/app/premium_channel.apd b/app/premium_channel.apd new file mode 100644 index 000000000..9764051d1 --- /dev/null +++ b/app/premium_channel.apd @@ -0,0 +1,6 @@ +version: 2 +url: $baseurl/connect/$nick +requires: local_channel +name: Premium Channel +photo: icon:check-circle-o +categories: Networking diff --git a/app/probe.apd b/app/probe.apd index c7b849ee1..097219292 100644 --- a/app/probe.apd +++ b/app/probe.apd @@ -1,6 +1,6 @@ -version: 1 +version: 2 url: $baseurl/probe requires: local_channel name: Remote Diagnostics photo: icon:user-md -categories: System +categories: Developer diff --git a/app/pubstream.apd b/app/pubstream.apd index ce9997126..4447ca750 100644 --- a/app/pubstream.apd +++ b/app/pubstream.apd @@ -1,4 +1,4 @@ -version: 2.2 +version: 2 url: $baseurl/pubstream requires: public_stream name: Public Stream diff --git a/app/randprof.apd b/app/randprof.apd index 1b2addd5c..60281d909 100644 --- a/app/randprof.apd +++ b/app/randprof.apd @@ -1,4 +1,4 @@ -version: 1 +version: 2 url: $baseurl/randprof name: Random Channel target: randprof diff --git a/app/search.apd b/app/search.apd index 9eb24ad41..462561f33 100644 --- a/app/search.apd +++ b/app/search.apd @@ -1,4 +1,4 @@ -version: 1 +version: 2 url: $baseurl/search name: Search photo: icon:search diff --git a/app/sources.apd b/app/sources.apd new file mode 100644 index 000000000..deeeae0a2 --- /dev/null +++ b/app/sources.apd @@ -0,0 +1,6 @@ +version: 2 +url: $baseurl/sources +requires: local_channel +name: Channel Sources +photo: icon:commenting-o +categories: Networking diff --git a/app/storage.apd b/app/storage.apd index cafcf81e8..ea15a2ef2 100644 --- a/app/storage.apd +++ b/app/storage.apd @@ -1,4 +1,4 @@ -version: 1 +version: 2 url: $baseurl/cloud/$nick requires: local_channel name: Files diff --git a/app/suggest.apd b/app/suggest.apd index 51b555264..0fdd8a399 100644 --- a/app/suggest.apd +++ b/app/suggest.apd @@ -1,4 +1,4 @@ -version: 1 +version: 2 url: $baseurl/suggest requires: local_channel name: Suggest Channels diff --git a/app/tokens.apd b/app/tokens.apd new file mode 100644 index 000000000..f271dc56c --- /dev/null +++ b/app/tokens.apd @@ -0,0 +1,6 @@ +version: 2 +url: $baseurl/tokens +requires: local_channel +name: Guest Access +photo: icon:user-secret +categories: Access Control diff --git a/app/uexport.apd b/app/uexport.apd new file mode 100644 index 000000000..773d74429 --- /dev/null +++ b/app/uexport.apd @@ -0,0 +1,6 @@ +version: 2 +url: $baseurl/uexport +requires: local_channel +name: Channel Export +photo: icon:download +categories: Personal, System diff --git a/app/webpages.apd b/app/webpages.apd index 46c6cdb5d..1c215512f 100644 --- a/app/webpages.apd +++ b/app/webpages.apd @@ -1,6 +1,6 @@ -version: 1 +version: 2 url: $baseurl/webpages/$nick -requires: local_channel, webpages +requires: local_channel name: Webpages photo: icon:newspaper-o categories: nav_featured_app, Productivity diff --git a/app/wiki.apd b/app/wiki.apd index 48fcbe0c1..e2fbe77e1 100644 --- a/app/wiki.apd +++ b/app/wiki.apd @@ -1,6 +1,6 @@ -version: 1 +version: 2 url: $baseurl/wiki/$nick -requires: local_channel, wiki +requires: local_channel name: Wiki photo: icon:pencil-square-o categories: nav_featured_app, Productivity diff --git a/boot.php b/boot.php index 815b0a113..ccd5cf62d 100755 --- a/boot.php +++ b/boot.php @@ -50,11 +50,11 @@ require_once('include/attach.php'); require_once('include/bbcode.php'); define ( 'PLATFORM_NAME', 'hubzilla' ); -define ( 'STD_VERSION', '3.6.1' ); +define ( 'STD_VERSION', '3.8' ); define ( 'ZOT_REVISION', '6.0a' ); -define ( 'DB_UPDATE_VERSION', 1216 ); +define ( 'DB_UPDATE_VERSION', 1224 ); define ( 'PROJECT_BASE', __DIR__ ); @@ -424,6 +424,7 @@ define ( 'TERM_BOOKMARK', 8 ); define ( 'TERM_HIERARCHY', 9 ); define ( 'TERM_COMMUNITYTAG', 10 ); define ( 'TERM_FORUM', 11 ); +define ( 'TERM_EMOJI', 12 ); define ( 'TERM_OBJ_POST', 1 ); define ( 'TERM_OBJ_PHOTO', 2 ); @@ -728,6 +729,11 @@ class App { private static $perms = null; // observer permissions private static $widgets = array(); // widgets for this page public static $config = array(); // config cache + public static $override_intltext_templates = array(); + public static $override_markup_templates = array(); + public static $override_templateroot = null; + public static $override_helproot = null; + public static $override_helpfiles = array(); public static $session = null; public static $groups; @@ -1050,7 +1056,6 @@ class App { self::$observer = $xchan; } - public static function get_observer() { return self::$observer; } @@ -1113,8 +1118,12 @@ class App { if(! x(self::$page,'title')) self::$page['title'] = self::$config['system']['sitename']; - if(! self::$meta->get_field('og:title')) - self::$meta->set('og:title',self::$page['title']); + $pagemeta = [ 'og:title' => self::$page['title'] ]; + + call_hooks('page_meta',$pagemeta); + foreach ($pagemeta as $metaproperty => $metavalue) { + self::$meta->set($metaproperty,$metavalue); + } self::$meta->set('generator', Zotlabs\Lib\System::get_platform_name()); @@ -1785,6 +1794,10 @@ function info($s) { return; if(! x($_SESSION, 'sysmsg_info')) $_SESSION['sysmsg_info'] = array(); + + if(in_array($s, $_SESSION['sysmsg_info'])) + return; + if(App::$interactive) $_SESSION['sysmsg_info'][] = $s; } @@ -2072,8 +2085,8 @@ function load_pdl() { if (! count(App::$layout)) { $arr = [ - 'module' => App::$module, - 'layout' => '' + 'module' => App::$module, + 'layout' => '' ]; /** * @hooks load_pdl @@ -2093,6 +2106,16 @@ function load_pdl() { if((! $s) && (($p = theme_include($n)) != '')) $s = @file_get_contents($p); + elseif(file_exists('addon/'. App::$module . '/' . $n)) + $s = @file_get_contents('addon/'. App::$module . '/' . $n); + + $arr = [ + 'module' => App::$module, + 'layout' => $s + ]; + call_hooks('alter_pdl',$arr); + $s = $arr['layout']; + if($s) { App::$comanche->parse($s); App::$pdl = $s; diff --git a/composer.json b/composer.json index 792c08810..b2aec5332 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,9 @@ "pear/text_languagedetect": "^1.0", "commerceguys/intl": "~0.7", "lukasreschke/id3parser": "^0.0.1", - "smarty/smarty": "~3.1" + "smarty/smarty": "~3.1", + "ramsey/uuid": "^3.8", + "twbs/bootstrap": "4.1.3" }, "require-dev" : { "phpunit/phpunit" : "@stable", diff --git a/composer.lock b/composer.lock index 6f1af0fd7..d352ad29a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ca5770d3c97cc1d0375413eeb61758ab", + "content-hash": "b7862124a9afe837c7eef8ee66f02ff4", "packages": [ { "name": "bshaffer/oauth2-server-php", @@ -157,16 +157,16 @@ }, { "name": "league/html-to-markdown", - "version": "4.6.2", + "version": "4.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/html-to-markdown.git", - "reference": "3af14d8f44838257a75822819784e83819b34e2e" + "reference": "76c076483cef89860d32a3fd25312f5a42848a8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/3af14d8f44838257a75822819784e83819b34e2e", - "reference": "3af14d8f44838257a75822819784e83819b34e2e", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/76c076483cef89860d32a3fd25312f5a42848a8c", + "reference": "76c076483cef89860d32a3fd25312f5a42848a8c", "shasum": "" }, "require": { @@ -185,7 +185,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.7-dev" + "dev-master": "4.8-dev" } }, "autoload": { @@ -217,7 +217,7 @@ "html", "markdown" ], - "time": "2018-01-07T19:45:06+00:00" + "time": "2018-05-19T23:47:12+00:00" }, { "name": "lukasreschke/id3parser", @@ -300,6 +300,51 @@ ], "time": "2018-01-15T00:49:33+00:00" }, + { + "name": "paragonie/random_compat", + "version": "v9.99.99", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "shasum": "" + }, + "require": { + "php": "^7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "time": "2018-07-02T15:55:56+00:00" + }, { "name": "pear/text_languagedetect", "version": "v1.0.0", @@ -391,6 +436,88 @@ ], "time": "2016-10-10T12:19:37+00:00" }, + { + "name": "ramsey/uuid", + "version": "3.8.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/d09ea80159c1929d75b3f9c60504d613aeb4a1e3", + "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "^1.0|^2.0|9.99.99", + "php": "^5.4 || ^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "codeception/aspect-mock": "^1.0 | ~2.0.0", + "doctrine/annotations": "~1.2.0", + "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ~2.1.0", + "ircmaxell/random-lib": "^1.1", + "jakub-onderka/php-parallel-lint": "^0.9.0", + "mockery/mockery": "^0.9.9", + "moontoast/math": "^1.1", + "php-mock/php-mock-phpunit": "^0.3|^1.1", + "phpunit/phpunit": "^4.7|^5.0|^6.5", + "squizlabs/php_codesniffer": "^2.3" + }, + "suggest": { + "ext-ctype": "Provides support for PHP Ctype functions", + "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", + "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", + "ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", + "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marijn Huizendveld", + "email": "marijn.huizendveld@gmail.com" + }, + { + "name": "Thibaud Fabre", + "email": "thibaud@aztech.io" + }, + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", + "homepage": "https://github.com/ramsey/uuid", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "time": "2018-07-19T23:38:55+00:00" + }, { "name": "sabre/dav", "version": "3.2.2", @@ -800,25 +927,32 @@ }, { "name": "simplepie/simplepie", - "version": "1.5.1", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/simplepie/simplepie.git", - "reference": "db9fff27b6d49eed3d4047cd3211ec8dba2f5d6e" + "reference": "0e8fe72132dad765d25db4cabc69a91139af1263" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplepie/simplepie/zipball/db9fff27b6d49eed3d4047cd3211ec8dba2f5d6e", - "reference": "db9fff27b6d49eed3d4047cd3211ec8dba2f5d6e", + "url": "https://api.github.com/repos/simplepie/simplepie/zipball/0e8fe72132dad765d25db4cabc69a91139af1263", + "reference": "0e8fe72132dad765d25db4cabc69a91139af1263", "shasum": "" }, "require": { - "php": ">=5.3.0" + "ext-pcre": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "php": ">=5.6.0" }, "require-dev": { - "phpunit/phpunit": "~4 || ~5" + "phpunit/phpunit": "~5.4.3 || ~6.5" }, "suggest": { + "ext-curl": "", + "ext-iconv": "", + "ext-intl": "", + "ext-mbstring": "", "mf2/mf2": "Microformat module that allows for parsing HTML for microformats" }, "type": "library", @@ -827,6 +961,11 @@ "SimplePie": "library" } }, + "scripts": { + "test": [ + "phpunit" + ] + }, "license": [ "BSD-3-Clause" ], @@ -856,10 +995,10 @@ "rss" ], "support": { - "source": "https://github.com/simplepie/simplepie/tree/1.5.1", + "source": "https://github.com/simplepie/simplepie/tree/1.5.2", "issues": "https://github.com/simplepie/simplepie/issues" }, - "time": "2017-11-12T02:03:34+00:00" + "time": "2018-08-02T05:43:58+00:00" }, { "name": "smarty/smarty", @@ -913,21 +1052,130 @@ "templating" ], "time": "2018-04-24T14:53:33+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-08-06T14:22:27+00:00" + }, + { + "name": "twbs/bootstrap", + "version": "v4.1.3", + "source": { + "type": "git", + "url": "https://github.com/twbs/bootstrap.git", + "reference": "3b558734382ce58b51e5fc676453bfd53bba9201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/3b558734382ce58b51e5fc676453bfd53bba9201", + "reference": "3b558734382ce58b51e5fc676453bfd53bba9201", + "shasum": "" + }, + "replace": { + "twitter/bootstrap": "self.version" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jacob Thornton", + "email": "jacobthornton@gmail.com" + }, + { + "name": "Mark Otto", + "email": "markdotto@gmail.com" + } + ], + "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", + "homepage": "https://getbootstrap.com/", + "keywords": [ + "JS", + "css", + "framework", + "front-end", + "mobile-first", + "responsive", + "sass", + "web" + ], + "time": "2018-07-24T15:54:34+00:00" } ], "packages-dev": [ { "name": "behat/behat", - "version": "v3.4.3", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/Behat/Behat.git", - "reference": "d60b161bff1b95ec4bb80bb8cb210ccf890314c2" + "reference": "e4bce688be0c2029dc1700e46058d86428c63cab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Behat/zipball/d60b161bff1b95ec4bb80bb8cb210ccf890314c2", - "reference": "d60b161bff1b95ec4bb80bb8cb210ccf890314c2", + "url": "https://api.github.com/repos/Behat/Behat/zipball/e4bce688be0c2029dc1700e46058d86428c63cab", + "reference": "e4bce688be0c2029dc1700e46058d86428c63cab", "shasum": "" }, "require": { @@ -937,9 +1185,9 @@ "ext-mbstring": "*", "php": ">=5.3.3", "psr/container": "^1.0", - "symfony/class-loader": "~2.1||~3.0||~4.0", + "symfony/class-loader": "~2.1||~3.0", "symfony/config": "~2.3||~3.0||~4.0", - "symfony/console": "~2.5||~3.0||~4.0", + "symfony/console": "~2.7.40||^2.8.33||~3.3.15||^3.4.3||^4.0.3", "symfony/dependency-injection": "~2.1||~3.0||~4.0", "symfony/event-dispatcher": "~2.1||~3.0||~4.0", "symfony/translation": "~2.3||~3.0||~4.0", @@ -950,18 +1198,13 @@ "phpunit/phpunit": "^4.8.36|^6.3", "symfony/process": "~2.5|~3.0|~4.0" }, - "suggest": { - "behat/mink-extension": "for integration with Mink testing framework", - "behat/symfony2-extension": "for integration with Symfony2 web framework", - "behat/yii-extension": "for integration with Yii web framework" - }, "bin": [ "bin/behat" ], "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2.x-dev" + "dev-master": "3.5.x-dev" } }, "autoload": { @@ -997,7 +1240,7 @@ "symfony", "testing" ], - "time": "2017-11-27T10:37:56+00:00" + "time": "2018-08-10T18:56:51+00:00" }, { "name": "behat/gherkin", @@ -1363,32 +1606,32 @@ }, { "name": "doctrine/instantiator", - "version": "1.0.5", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", "shasum": "" }, "require": { - "php": ">=5.3,<8.0-DEV" + "php": "^7.1" }, "require-dev": { "athletic/athletic": "~0.1.8", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" + "phpunit/phpunit": "^6.2.3", + "squizlabs/php_codesniffer": "^3.0.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -1413,20 +1656,20 @@ "constructor", "instantiate" ], - "time": "2015-06-14T21:17:01+00:00" + "time": "2017-07-22T11:58:36+00:00" }, { "name": "fabpot/goutte", - "version": "v3.2.2", + "version": "v3.2.3", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/Goutte.git", - "reference": "395f61d7c2e15a813839769553a4de16fa3b3c96" + "reference": "3f0eaf0a40181359470651f1565b3e07e3dd31b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/395f61d7c2e15a813839769553a4de16fa3b3c96", - "reference": "395f61d7c2e15a813839769553a4de16fa3b3c96", + "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/3f0eaf0a40181359470651f1565b3e07e3dd31b8", + "reference": "3f0eaf0a40181359470651f1565b3e07e3dd31b8", "shasum": "" }, "require": { @@ -1468,7 +1711,7 @@ "keywords": [ "scraper" ], - "time": "2017-11-19T08:45:40+00:00" + "time": "2018-06-29T15:13:57+00:00" }, { "name": "guzzlehttp/guzzle", @@ -1653,25 +1896,28 @@ }, { "name": "myclabs/deep-copy", - "version": "1.7.0", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", - "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", + "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.1" + }, + "replace": { + "myclabs/deep-copy": "self.version" }, "require-dev": { "doctrine/collections": "^1.0", "doctrine/common": "^2.6", - "phpunit/phpunit": "^4.1" + "phpunit/phpunit": "^7.1" }, "type": "library", "autoload": { @@ -1694,26 +1940,26 @@ "object", "object graph" ], - "time": "2017-10-19T19:58:43+00:00" + "time": "2018-06-11T23:09:50+00:00" }, { "name": "phar-io/manifest", - "version": "1.0.1", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", "shasum": "" }, "require": { "ext-dom": "*", "ext-phar": "*", - "phar-io/version": "^1.0.1", + "phar-io/version": "^2.0", "php": "^5.6 || ^7.0" }, "type": "library", @@ -1749,20 +1995,20 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "time": "2017-03-05T18:14:27+00:00" + "time": "2018-07-08T19:23:20+00:00" }, { "name": "phar-io/version", - "version": "1.0.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", "shasum": "" }, "require": { @@ -1796,7 +2042,7 @@ } ], "description": "Library for handling version information and constraints", - "time": "2017-03-05T17:38:23+00:00" + "time": "2018-07-08T19:19:57+00:00" }, { "name": "php-mock/php-mock", @@ -2022,29 +2268,35 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.3.2", + "version": "4.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2" + "reference": "94fd0001232e47129dd3504189fa1c7225010d08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bf329f6c1aadea3299f08ee804682b7c45b326a2", - "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", + "reference": "94fd0001232e47129dd3504189fa1c7225010d08", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", + "php": "^7.0", "phpdocumentor/reflection-common": "^1.0.0", "phpdocumentor/type-resolver": "^0.4.0", "webmozart/assert": "^1.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" + "doctrine/instantiator": "~1.0.5", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, "autoload": { "psr-4": { "phpDocumentor\\Reflection\\": [ @@ -2063,7 +2315,7 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-11-10T14:09:06+00:00" + "time": "2017-11-30T07:14:17+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -2114,16 +2366,16 @@ }, { "name": "phpspec/prophecy", - "version": "1.7.6", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712" + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", "shasum": "" }, "require": { @@ -2135,12 +2387,12 @@ }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.8.x-dev" } }, "autoload": { @@ -2173,33 +2425,33 @@ "spy", "stub" ], - "time": "2018-04-18T13:57:24+00:00" + "time": "2018-08-05T17:53:17+00:00" }, { "name": "phpunit/dbunit", - "version": "3.0.3", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/dbunit.git", - "reference": "0fa4329e490480ab957fe7b1185ea0996ca11f44" + "reference": "e77b469c3962b5a563f09a2a989f1c9bd38b8615" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/0fa4329e490480ab957fe7b1185ea0996ca11f44", - "reference": "0fa4329e490480ab957fe7b1185ea0996ca11f44", + "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/e77b469c3962b5a563f09a2a989f1c9bd38b8615", + "reference": "e77b469c3962b5a563f09a2a989f1c9bd38b8615", "shasum": "" }, "require": { "ext-pdo": "*", "ext-simplexml": "*", - "php": "^7.0", - "phpunit/phpunit": "^6.0", + "php": "^7.1", + "phpunit/phpunit": "^7.0", "symfony/yaml": "^3.0 || ^4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -2225,44 +2477,44 @@ "testing", "xunit" ], - "time": "2018-01-23T13:32:26+00:00" + "time": "2018-02-07T06:47:59+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "5.3.2", + "version": "6.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "c89677919c5dd6d3b3852f230a663118762218ac" + "reference": "865662550c384bc1db7e51d29aeda1c2c161d69a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac", - "reference": "c89677919c5dd6d3b3852f230a663118762218ac", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/865662550c384bc1db7e51d29aeda1c2c161d69a", + "reference": "865662550c384bc1db7e51d29aeda1c2c161d69a", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^7.0", - "phpunit/php-file-iterator": "^1.4.2", + "php": "^7.1", + "phpunit/php-file-iterator": "^2.0", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^2.0.1", + "phpunit/php-token-stream": "^3.0", "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^3.0", + "sebastian/environment": "^3.1", "sebastian/version": "^2.0.1", "theseer/tokenizer": "^1.1" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^7.0" }, "suggest": { - "ext-xdebug": "^2.5.5" + "ext-xdebug": "^2.6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.3.x-dev" + "dev-master": "6.0-dev" } }, "autoload": { @@ -2288,29 +2540,29 @@ "testing", "xunit" ], - "time": "2018-04-06T15:36:58+00:00" + "time": "2018-06-01T07:51:50+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "1.4.5", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + "reference": "cecbc684605bb0cc288828eb5d65d93d5c676d3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cecbc684605bb0cc288828eb5d65d93d5c676d3c", + "reference": "cecbc684605bb0cc288828eb5d65d93d5c676d3c", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -2325,7 +2577,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -2335,7 +2587,7 @@ "filesystem", "iterator" ], - "time": "2017-11-27T13:52:08+00:00" + "time": "2018-06-11T11:44:00+00:00" }, { "name": "phpunit/php-text-template", @@ -2380,28 +2632,28 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.9", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8b8454ea6958c3dee38453d3bd571e023108c91f", + "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -2416,7 +2668,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -2425,33 +2677,33 @@ "keywords": [ "timer" ], - "time": "2017-02-26T11:10:40+00:00" + "time": "2018-02-01T13:07:23+00:00" }, { "name": "phpunit/php-token-stream", - "version": "2.0.2", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "791198a2c6254db10131eecfe8c06670700904db" + "reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", - "reference": "791198a2c6254db10131eecfe8c06670700904db", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/21ad88bbba7c3d93530d93994e0a33cd45f02ace", + "reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": "^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^6.2.4" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -2474,40 +2726,40 @@ "keywords": [ "tokenizer" ], - "time": "2017-11-27T05:48:46+00:00" + "time": "2018-02-01T13:16:43+00:00" }, { "name": "phpunit/phpunit", - "version": "6.5.8", + "version": "7.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b" + "reference": "34705f81bddc3f505b9599a2ef96e2b4315ba9b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4f21a3c6b97c42952fd5c2837bb354ec0199b97b", - "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/34705f81bddc3f505b9599a2ef96e2b4315ba9b8", + "reference": "34705f81bddc3f505b9599a2ef96e2b4315ba9b8", "shasum": "" }, "require": { + "doctrine/instantiator": "^1.1", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", - "myclabs/deep-copy": "^1.6.1", - "phar-io/manifest": "^1.0.1", - "phar-io/version": "^1.0", - "php": "^7.0", + "myclabs/deep-copy": "^1.7", + "phar-io/manifest": "^1.0.2", + "phar-io/version": "^2.0", + "php": "^7.1", "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^5.3", - "phpunit/php-file-iterator": "^1.4.3", + "phpunit/php-code-coverage": "^6.0.7", + "phpunit/php-file-iterator": "^2.0.1", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^1.0.9", - "phpunit/phpunit-mock-objects": "^5.0.5", - "sebastian/comparator": "^2.1", - "sebastian/diff": "^2.0", + "phpunit/php-timer": "^2.0", + "sebastian/comparator": "^3.0", + "sebastian/diff": "^3.0", "sebastian/environment": "^3.1", "sebastian/exporter": "^3.1", "sebastian/global-state": "^2.0", @@ -2516,15 +2768,15 @@ "sebastian/version": "^2.0.1" }, "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2", - "phpunit/dbunit": "<3.0" + "phpunit/phpunit-mock-objects": "*" }, "require-dev": { "ext-pdo": "*" }, "suggest": { + "ext-soap": "*", "ext-xdebug": "*", - "phpunit/php-invoker": "^1.1" + "phpunit/php-invoker": "^2.0" }, "bin": [ "phpunit" @@ -2532,7 +2784,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "6.5.x-dev" + "dev-master": "7.3-dev" } }, "autoload": { @@ -2558,66 +2810,7 @@ "testing", "xunit" ], - "time": "2018-04-10T11:38:34+00:00" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "5.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf", - "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.5", - "php": "^7.0", - "phpunit/php-text-template": "^1.2.1", - "sebastian/exporter": "^3.1" - }, - "conflict": { - "phpunit/phpunit": "<6.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.5" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "time": "2018-01-06T05:45:45+00:00" + "time": "2018-08-22T06:39:21+00:00" }, { "name": "psr/container", @@ -2765,30 +2958,30 @@ }, { "name": "sebastian/comparator", - "version": "2.1.3", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", "shasum": "" }, "require": { - "php": "^7.0", - "sebastian/diff": "^2.0 || ^3.0", + "php": "^7.1", + "sebastian/diff": "^3.0", "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "^6.4" + "phpunit/phpunit": "^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -2825,32 +3018,33 @@ "compare", "equality" ], - "time": "2018-02-01T13:46:46+00:00" + "time": "2018-07-12T15:12:46+00:00" }, { "name": "sebastian/diff", - "version": "2.0.1", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" + "reference": "366541b989927187c4ca70490a35615d3fef2dce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/366541b989927187c4ca70490a35615d3fef2dce", + "reference": "366541b989927187c4ca70490a35615d3fef2dce", "shasum": "" }, "require": { - "php": "^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^6.2" + "phpunit/phpunit": "^7.0", + "symfony/process": "^2 || ^3.3 || ^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -2875,9 +3069,12 @@ "description": "Diff implementation", "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ - "diff" + "diff", + "udiff", + "unidiff", + "unified diff" ], - "time": "2017-08-03T08:09:46+00:00" + "time": "2018-06-10T07:54:39+00:00" }, { "name": "sebastian/environment", @@ -3279,25 +3476,25 @@ }, { "name": "symfony/browser-kit", - "version": "v3.4.9", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79" + "reference": "c55fe9257003b2d95c0211b3f6941e8dfd26dffd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/840bb6f0d5b3701fd768b68adf7193c2d0f98f79", - "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/c55fe9257003b2d95c0211b3f6941e8dfd26dffd", + "reference": "c55fe9257003b2d95c0211b3f6941e8dfd26dffd", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/dom-crawler": "~2.8|~3.0|~4.0" + "php": "^7.1.3", + "symfony/dom-crawler": "~3.4|~4.0" }, "require-dev": { - "symfony/css-selector": "~2.8|~3.0|~4.0", - "symfony/process": "~2.8|~3.0|~4.0" + "symfony/css-selector": "~3.4|~4.0", + "symfony/process": "~3.4|~4.0" }, "suggest": { "symfony/process": "" @@ -3305,7 +3502,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -3332,20 +3529,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2018-03-19T22:32:39+00:00" + "time": "2018-07-26T09:10:45+00:00" }, { "name": "symfony/class-loader", - "version": "v3.4.9", + "version": "v3.4.15", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", - "reference": "e63c12699822bb3b667e7216ba07fbcc3a3e203e" + "reference": "31db283fc86d3143e7ff87e922177b457d909c30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/e63c12699822bb3b667e7216ba07fbcc3a3e203e", - "reference": "e63c12699822bb3b667e7216ba07fbcc3a3e203e", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/31db283fc86d3143e7ff87e922177b457d909c30", + "reference": "31db283fc86d3143e7ff87e922177b457d909c30", "shasum": "" }, "require": { @@ -3388,35 +3585,35 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:37:34+00:00" + "time": "2018-07-26T11:19:56+00:00" }, { "name": "symfony/config", - "version": "v3.4.9", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "7c2a9d44f4433863e9bca682e7f03609234657f9" + "reference": "76015a3cc372b14d00040ff58e18e29f69eba717" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/7c2a9d44f4433863e9bca682e7f03609234657f9", - "reference": "7c2a9d44f4433863e9bca682e7f03609234657f9", + "url": "https://api.github.com/repos/symfony/config/zipball/76015a3cc372b14d00040ff58e18e29f69eba717", + "reference": "76015a3cc372b14d00040ff58e18e29f69eba717", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/filesystem": "~2.8|~3.0|~4.0" + "php": "^7.1.3", + "symfony/filesystem": "~3.4|~4.0", + "symfony/polyfill-ctype": "~1.8" }, "conflict": { - "symfony/dependency-injection": "<3.3", - "symfony/finder": "<3.3" + "symfony/finder": "<3.4" }, "require-dev": { - "symfony/dependency-injection": "~3.3|~4.0", - "symfony/event-dispatcher": "~3.3|~4.0", - "symfony/finder": "~3.3|~4.0", - "symfony/yaml": "~3.0|~4.0" + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~3.4|~4.0", + "symfony/finder": "~3.4|~4.0", + "symfony/yaml": "~3.4|~4.0" }, "suggest": { "symfony/yaml": "To use the yaml reference dumper" @@ -3424,7 +3621,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -3451,25 +3648,24 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2018-03-19T22:32:39+00:00" + "time": "2018-08-08T06:37:38+00:00" }, { "name": "symfony/console", - "version": "v3.4.9", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5b1fdfa8eb93464bcc36c34da39cedffef822cdf" + "reference": "ca80b8ced97cf07390078b29773dc384c39eee1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5b1fdfa8eb93464bcc36c34da39cedffef822cdf", - "reference": "5b1fdfa8eb93464bcc36c34da39cedffef822cdf", + "url": "https://api.github.com/repos/symfony/console/zipball/ca80b8ced97cf07390078b29773dc384c39eee1f", + "reference": "ca80b8ced97cf07390078b29773dc384c39eee1f", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/debug": "~2.8|~3.0|~4.0", + "php": "^7.1.3", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { @@ -3478,11 +3674,11 @@ }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "~3.3|~4.0", + "symfony/config": "~3.4|~4.0", "symfony/dependency-injection": "~3.4|~4.0", - "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/event-dispatcher": "~3.4|~4.0", "symfony/lock": "~3.4|~4.0", - "symfony/process": "~3.3|~4.0" + "symfony/process": "~3.4|~4.0" }, "suggest": { "psr/log-implementation": "For using the console logger", @@ -3493,7 +3689,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -3520,20 +3716,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-04-30T01:22:56+00:00" + "time": "2018-07-26T11:24:31+00:00" }, { "name": "symfony/css-selector", - "version": "v3.4.9", + "version": "v3.4.15", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "519a80d7c1d95c6cc0b67f686d15fe27c6910de0" + "reference": "edda5a6155000ff8c3a3f85ee5c421af93cca416" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/519a80d7c1d95c6cc0b67f686d15fe27c6910de0", - "reference": "519a80d7c1d95c6cc0b67f686d15fe27c6910de0", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/edda5a6155000ff8c3a3f85ee5c421af93cca416", + "reference": "edda5a6155000ff8c3a3f85ee5c421af93cca416", "shasum": "" }, "require": { @@ -3573,85 +3769,29 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2018-03-19T22:32:39+00:00" - }, - { - "name": "symfony/debug", - "version": "v3.4.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/debug.git", - "reference": "1b95888cfd996484527cb41e8952d9a5eaf7454f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/1b95888cfd996484527cb41e8952d9a5eaf7454f", - "reference": "1b95888cfd996484527cb41e8952d9a5eaf7454f", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8", - "psr/log": "~1.0" - }, - "conflict": { - "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" - }, - "require-dev": { - "symfony/http-kernel": "~2.8|~3.0|~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Debug\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Debug Component", - "homepage": "https://symfony.com", - "time": "2018-04-30T16:53:52+00:00" + "time": "2018-07-26T09:06:28+00:00" }, { "name": "symfony/dependency-injection", - "version": "v3.4.9", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "54ff9d78b56429f9a1ac12e60bfb6d169c0468e3" + "reference": "bae4983003c9d451e278504d7d9b9d7fc1846873" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/54ff9d78b56429f9a1ac12e60bfb6d169c0468e3", - "reference": "54ff9d78b56429f9a1ac12e60bfb6d169c0468e3", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/bae4983003c9d451e278504d7d9b9d7fc1846873", + "reference": "bae4983003c9d451e278504d7d9b9d7fc1846873", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", + "php": "^7.1.3", "psr/container": "^1.0" }, "conflict": { - "symfony/config": "<3.3.7", - "symfony/finder": "<3.3", + "symfony/config": "<4.1.1", + "symfony/finder": "<3.4", "symfony/proxy-manager-bridge": "<3.4", "symfony/yaml": "<3.4" }, @@ -3659,8 +3799,8 @@ "psr/container-implementation": "1.0" }, "require-dev": { - "symfony/config": "~3.3|~4.0", - "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/config": "~4.1", + "symfony/expression-language": "~3.4|~4.0", "symfony/yaml": "~3.4|~4.0" }, "suggest": { @@ -3673,7 +3813,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -3700,28 +3840,29 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2018-04-29T14:04:08+00:00" + "time": "2018-08-08T11:48:58+00:00" }, { "name": "symfony/dom-crawler", - "version": "v3.4.9", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "1a4cffeb059226ff6bee9f48acb388faf674afff" + "reference": "1c4519d257e652404c3aa550207ccd8ada66b38e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/1a4cffeb059226ff6bee9f48acb388faf674afff", - "reference": "1a4cffeb059226ff6bee9f48acb388faf674afff", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/1c4519d257e652404c3aa550207ccd8ada66b38e", + "reference": "1c4519d257e652404c3aa550207ccd8ada66b38e", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", + "php": "^7.1.3", + "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { - "symfony/css-selector": "~2.8|~3.0|~4.0" + "symfony/css-selector": "~3.4|~4.0" }, "suggest": { "symfony/css-selector": "" @@ -3729,7 +3870,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -3756,34 +3897,34 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2018-03-19T22:32:39+00:00" + "time": "2018-07-26T11:00:49+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v3.4.9", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "fdd5abcebd1061ec647089c6c41a07ed60af09f8" + "reference": "bfb30c2ad377615a463ebbc875eba64a99f6aa3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/fdd5abcebd1061ec647089c6c41a07ed60af09f8", - "reference": "fdd5abcebd1061ec647089c6c41a07ed60af09f8", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/bfb30c2ad377615a463ebbc875eba64a99f6aa3e", + "reference": "bfb30c2ad377615a463ebbc875eba64a99f6aa3e", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8" + "php": "^7.1.3" }, "conflict": { - "symfony/dependency-injection": "<3.3" + "symfony/dependency-injection": "<3.4" }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "~2.8|~3.0|~4.0", - "symfony/dependency-injection": "~3.3|~4.0", - "symfony/expression-language": "~2.8|~3.0|~4.0", - "symfony/stopwatch": "~2.8|~3.0|~4.0" + "symfony/config": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/expression-language": "~3.4|~4.0", + "symfony/stopwatch": "~3.4|~4.0" }, "suggest": { "symfony/dependency-injection": "", @@ -3792,7 +3933,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -3819,29 +3960,30 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2018-04-06T07:35:25+00:00" + "time": "2018-07-26T09:10:45+00:00" }, { "name": "symfony/filesystem", - "version": "v3.4.9", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541" + "reference": "c0f5f62db218fa72195b8b8700e4b9b9cf52eb5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/253a4490b528597aa14d2bf5aeded6f5e5e4a541", - "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/c0f5f62db218fa72195b8b8700e4b9b9cf52eb5e", + "reference": "c0f5f62db218fa72195b8b8700e4b9b9cf52eb5e", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8" + "php": "^7.1.3", + "symfony/polyfill-ctype": "~1.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -3868,20 +4010,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2018-02-22T10:48:49+00:00" + "time": "2018-08-18T16:52:46+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.8.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "3296adf6a6454a050679cde90f95350ad604b171" + "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171", - "reference": "3296adf6a6454a050679cde90f95350ad604b171", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8", + "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8", "shasum": "" }, "require": { @@ -3893,7 +4035,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -3927,37 +4069,38 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/translation", - "version": "v3.4.9", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "d4af50f46cd8171fd5c1cdebdb9a8bbcd8078c6c" + "reference": "fa2182669f7983b7aa5f1a770d053f79f0ef144f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/d4af50f46cd8171fd5c1cdebdb9a8bbcd8078c6c", - "reference": "d4af50f46cd8171fd5c1cdebdb9a8bbcd8078c6c", + "url": "https://api.github.com/repos/symfony/translation/zipball/fa2182669f7983b7aa5f1a770d053f79f0ef144f", + "reference": "fa2182669f7983b7aa5f1a770d053f79f0ef144f", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", + "php": "^7.1.3", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/config": "<2.8", + "symfony/config": "<3.4", "symfony/dependency-injection": "<3.4", "symfony/yaml": "<3.4" }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "~2.8|~3.0|~4.0", + "symfony/config": "~3.4|~4.0", + "symfony/console": "~3.4|~4.0", "symfony/dependency-injection": "~3.4|~4.0", "symfony/finder": "~2.8|~3.0|~4.0", - "symfony/intl": "^2.8.18|^3.2.5|~4.0", + "symfony/intl": "~3.4|~4.0", "symfony/yaml": "~3.4|~4.0" }, "suggest": { @@ -3968,7 +4111,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -3995,24 +4138,25 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2018-04-30T01:22:56+00:00" + "time": "2018-08-07T12:45:11+00:00" }, { "name": "symfony/yaml", - "version": "v3.4.9", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "033cfa61ef06ee0847e056e530201842b6e926c3" + "reference": "b832cc289608b6d305f62149df91529a2ab3c314" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/033cfa61ef06ee0847e056e530201842b6e926c3", - "reference": "033cfa61ef06ee0847e056e530201842b6e926c3", + "url": "https://api.github.com/repos/symfony/yaml/zipball/b832cc289608b6d305f62149df91529a2ab3c314", + "reference": "b832cc289608b6d305f62149df91529a2ab3c314", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8" + "php": "^7.1.3", + "symfony/polyfill-ctype": "~1.8" }, "conflict": { "symfony/console": "<3.4" @@ -4026,7 +4170,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -4053,7 +4197,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2018-04-08T08:21:29+00:00" + "time": "2018-08-18T16:52:46+00:00" }, { "name": "theseer/tokenizer", diff --git a/doc/bugs.bb b/doc/bugs.bb index f6e14b659..f773da025 100644 --- a/doc/bugs.bb +++ b/doc/bugs.bb @@ -1,5 +1,5 @@ [h2]Bugs, Issues, and things that go bump in the night...[/h2] -[h3]Something went wrong! Who is charge of fixing it?[/h3] +[h3]Something went wrong! Who is in charge of fixing it?[/h3] [b]$Projectname Community Server[/b] @@ -23,7 +23,7 @@ If you get a blank white screen when doing something, this is almost always a co At this point it might be worthwhile discussing the issue on one of the online forums. There may be several of these and some may be more suited to your spoken language. At this time, the 'Hubzilla Support' channel (support@gravizot.de) is the recommended forum for discussing bugs. -If community members with software engineering training/expertise can't help you right away, understand that they are volunteers and may have a lot of other work and demands on their time. At this point you need to file a bug report. You will need an account on github.com to do this. So register, and then visit https://framagit.org/hubzilla/core/issues . Create an issue here and provide all the same information that you provided online. Don't leave out anything. +If community members with software engineering training/expertise can't help you right away, understand that they are volunteers and may have a lot of other work and demands on their time. At this point you need to file a bug report. You will need an account on framagit.org to do this. So register, and then visit https://framagit.org/hubzilla/core/issues . Create an issue here and provide all the same information that you provided online. Don't leave out anything. Then you wait. If it's a high profile issue, it may get fixed quickly. But nobody is in charge of fixing bugs. If it lingers without resolution, please spend some more time investigating the problem. Ask about anything you don't understand related to the behaviour. You will learn more about how the software works and quite possibly figure out why it isn't working now. Ultimately it is somebody in the community who is going to fix this and you are a member of the community; and this is how the open source process works. diff --git a/doc/database.bb b/doc/database.bb index 160ec505e..a72081e4a 100644 --- a/doc/database.bb +++ b/doc/database.bb @@ -16,8 +16,8 @@ [tr][td][zrl=[baseurl]/help/database/db_config]config[/zrl][/td][td]main configuration storage[/td][/tr] [tr][td][zrl=[baseurl]/help/database/db_conv]conv[/zrl][/td][td]Diaspora private messages meta conversation structure[/td][/tr] [tr][td][zrl=[baseurl]/help/database/db_event]event[/zrl][/td][td]Events[/td][/tr] -[tr][td][zrl=[baseurl]/help/database/db_group_member]group_member[/zrl][/td][td]privacy groups (collections), group info[/td][/tr] -[tr][td][zrl=[baseurl]/help/database/db_groups]groups[/zrl][/td][td]privacy groups (collections), member info[/td][/tr] +[tr][td][zrl=[baseurl]/help/database/db_pgrp_member]pgrp_member[/zrl][/td][td]privacy groups (collections), group info[/td][/tr] +[tr][td][zrl=[baseurl]/help/database/db_pgrp]pgrp[/zrl][/td][td]privacy groups (collections), member info[/td][/tr] [tr][td][zrl=[baseurl]/help/database/db_hook]hook[/zrl][/td][td]plugin hook registry[/td][/tr] [tr][td][zrl=[baseurl]/help/database/db_hubloc]hubloc[/zrl][/td][td]xchan location storage, ties a hub location to an xchan[/td][/tr] [tr][td][zrl=[baseurl]/help/database/db_iconfig]iconfig[/zrl][/td][td]extensible arbitrary storage for items[/td][/tr] diff --git a/doc/database/db_groups.bb b/doc/database/db_pgrp.bb similarity index 100% rename from doc/database/db_groups.bb rename to doc/database/db_pgrp.bb diff --git a/doc/database/db_group_member.bb b/doc/database/db_pgrp_member.bb similarity index 100% rename from doc/database/db_group_member.bb rename to doc/database/db_pgrp_member.bb diff --git a/doc/hook/activity_filter.bb b/doc/hook/activity_filter.bb new file mode 100644 index 000000000..9d0768577 --- /dev/null +++ b/doc/hook/activity_filter.bb @@ -0,0 +1 @@ +[h2]activity_filter[/h2] diff --git a/doc/hook/activity_order.bb b/doc/hook/activity_order.bb new file mode 100644 index 000000000..4a4670d03 --- /dev/null +++ b/doc/hook/activity_order.bb @@ -0,0 +1 @@ +[h2]activity_order[/h2] diff --git a/doc/hook/addon_app_installed_filter.bb b/doc/hook/addon_app_installed_filter.bb new file mode 100644 index 000000000..e610b3205 --- /dev/null +++ b/doc/hook/addon_app_installed_filter.bb @@ -0,0 +1,18 @@ +[h2]addon_app_installed_filter[/h2] + +Allow plugins to filter the result of addon_app_installed. + +Code excerpt: + +[code] + $filter_arr = [ + 'uid'=>$uid, + 'app'=>$app, + 'installed'=>$r + ]; + call_hooks('addon_app_installed_filter',$filter_arr); + $r = $filter_arr['installed']; +[/code] + +cxref: Zotlabs/Lib/Apps.php + diff --git a/doc/hook/app_destroy.bb b/doc/hook/app_destroy.bb new file mode 100644 index 000000000..386d7af16 --- /dev/null +++ b/doc/hook/app_destroy.bb @@ -0,0 +1,4 @@ +[h2]app_destroy[/h2] + +Allows addons to perform some post delete actions. + diff --git a/doc/hook/app_installed_filter.bb b/doc/hook/app_installed_filter.bb new file mode 100644 index 000000000..f0d91d6f0 --- /dev/null +++ b/doc/hook/app_installed_filter.bb @@ -0,0 +1,17 @@ +[h2]app_installed_filter[/h2] + +Allow plugins to filter the result of app_installed. + +Code excerpt: + +[code] + $filter_arr = [ + 'uid'=>$uid, + 'app'=>$app, + 'installed'=>$r + ]; + call_hooks('app_installed_filter',$filter_arr); + $r = $filter_arr['installed']; +[/code] + +cxref: Zotlabs/Lib/Apps.php diff --git a/doc/hook/attach_delete.bb b/doc/hook/attach_delete.bb new file mode 100644 index 000000000..3b63f28d3 --- /dev/null +++ b/doc/hook/attach_delete.bb @@ -0,0 +1,11 @@ +[h2]attach_delete[/h2] + +Invoked when an attachment is deleted using attach_delete(). + +[code] +$arr = ['channel_id' => $channel_id, 'resource' => $resource, 'is_photo'=>$is_photo]; +call_hooks("attach_delete",$arr); +[/code] + + +See include/attach.php diff --git a/doc/hook/content_security_policy.bb b/doc/hook/content_security_policy.bb new file mode 100644 index 000000000..96b8095ae --- /dev/null +++ b/doc/hook/content_security_policy.bb @@ -0,0 +1,39 @@ +[h2]content_security_policy[/h2] + +Called to modify CSP settings prior to the output of the Content-Security-Policy header. + +This hook permits addons to modify the content-security-policy if necessary to allow loading of foreign js libraries or css styles. + +[code] +if(App::$config['system']['content_security_policy']) { + $cspsettings = Array ( + 'script-src' => Array ("'self'","'unsafe-inline'","'unsafe-eval'"), + 'style-src' => Array ("'self'","'unsafe-inline'") + ); + call_hooks('content_security_policy',$cspsettings); + + // Legitimate CSP directives (cxref: https://content-security-policy.com/) + $validcspdirectives=Array( + "default-src", "script-src", "style-src", + "img-src", "connect-src", "font-src", + "object-src", "media-src", 'frame-src', + 'sandbox', 'report-uri', 'child-src', + 'form-action', 'frame-ancestors', 'plugin-types' + ); + $cspheader = "Content-Security-Policy:"; + foreach ($cspsettings as $cspdirective => $csp) { + if (!in_array($cspdirective,$validcspdirectives)) { + logger("INVALID CSP DIRECTIVE: ".$cspdirective,LOGGER_DEBUG); + continue; + } + $cspsettingsarray=array_unique($cspsettings[$cspdirective]); + $cspsetpolicy = implode(' ',$cspsettingsarray); + if ($cspsetpolicy) { + $cspheader .= " ".$cspdirective." ".$cspsetpolicy.";"; + } + } + header($cspheader); +} +[/code] + +see: boot.php diff --git a/doc/hook/dreport_process.bb b/doc/hook/dreport_process.bb new file mode 100644 index 000000000..3ad331f41 --- /dev/null +++ b/doc/hook/dreport_process.bb @@ -0,0 +1,7 @@ +[h2]dreport_process[/h2] + +Called for each delivery report received + +Passed a delivery_report array. + +see: include/zot.php diff --git a/doc/hook/dropdown_extras.bb b/doc/hook/dropdown_extras.bb new file mode 100644 index 000000000..6d7110a76 --- /dev/null +++ b/doc/hook/dropdown_extras.bb @@ -0,0 +1,17 @@ +[h2]dropdown_extras[/h2] + +Modify the dropdown menu available through the cog of items as displayed by conv_item.tpl + +This hook allows plugins to add arbitrary html to the cog dropdown of thread items displayed with the conv_item.tpl template. + +It is fed an array of ['item' => $item, 'dropdown_extras' => '']. Any additions to the cog menu should be prepended/appended to +the ['dropdown_extras'] element. + +[code] +$dropdown_extras_arr = [ 'item' => $item , 'dropdown_extras' => '' ]; +call_hooks('dropdown_extras',$dropdown_extras_arr); +$dropdown_extras = $dropdown_extras_arr['dropdown_extras']; +[/code] + +see: Zotlabs/Lib/ThreadItem.php +see: view/tpl/conv_item.tpl diff --git a/doc/hook/network_tabs.bb b/doc/hook/network_tabs.bb deleted file mode 100644 index 677d7f2b9..000000000 --- a/doc/hook/network_tabs.bb +++ /dev/null @@ -1 +0,0 @@ -[h2]network_tabs[/h2] diff --git a/doc/hook/page_meta.bb b/doc/hook/page_meta.bb new file mode 100644 index 000000000..30a8f9440 --- /dev/null +++ b/doc/hook/page_meta.bb @@ -0,0 +1,13 @@ +[h2]page_meta[/h2] + +Called before generating the page header. + +[code] + $pagemeta = [ 'og:title' => self::$page['title'] ]; + + call_hooks('page_meta',$pagemeta); + foreach ($pagemeta as $metaproperty => $metavalue) { + self::$meta->set($metaproperty,$metavalue); + } + +[/code] diff --git a/doc/hook/permit_hook.bb b/doc/hook/permit_hook.bb new file mode 100644 index 000000000..e69de29bb diff --git a/doc/hook/profile_tabs.bb b/doc/hook/profile_tabs.bb deleted file mode 100644 index 5b3e9e707..000000000 --- a/doc/hook/profile_tabs.bb +++ /dev/null @@ -1 +0,0 @@ -[h2]profile_tabs[/h2] diff --git a/doc/hook/status_editor.bb b/doc/hook/status_editor.bb new file mode 100644 index 000000000..00e97a7c9 --- /dev/null +++ b/doc/hook/status_editor.bb @@ -0,0 +1,31 @@ +[h2]status_editor[/h2] + +Replace the default status_editor (jot). + +Allow plugins to replace the default status editor in a context dependent manner. + +It is fed an array of ['editor_html' => '', 'x' => $x, 'popup' => $popup, 'module' => $module]. + +All calls to the status_editor at the time of the creation of this hook have been updated +to set $module at invocation. This allows addon developers to have a context dependent editor +based on the Hubzilla module/addon. + +Calls to status_editor() are in the form of: + status_editor($a, $x, $popup, $module). + +Future module/addon developers are encouraged to set $popup and $module when invoking the +status_editor. + + +[code] + $hook_info = ['editor_html' => '', 'x' => $x, 'popup' => $popup, 'module' => $module]; + call_hooks('status_editor',$hook_info); + if ($hook_info['editor_html'] == '') { + return hz_status_editor($a, $x, $popup); + } else { + return $hook_info['editor_html']; + } + +[/code] + +see: include/conversation.php diff --git a/doc/hook/system_app_installed_filter.bb b/doc/hook/system_app_installed_filter.bb new file mode 100644 index 000000000..a269a79a8 --- /dev/null +++ b/doc/hook/system_app_installed_filter.bb @@ -0,0 +1,18 @@ +[h2]system_app_installed_filter[/h2] + +Allow plugins to filter the result of system_app_installed. + +Code excerpt: + +[code] + $filter_arr = [ + 'uid'=>$uid, + 'app'=>$app, + 'installed'=>$r + ]; + call_hooks('system_app_installed_filter',$filter_arr); + $r = $filter_arr['installed']; +[/code] + +cxref: Zotlabs/Lib/Apps.php + diff --git a/doc/hook/wiki_preprocess.bb b/doc/hook/wiki_preprocess.bb new file mode 100644 index 000000000..913b601ba --- /dev/null +++ b/doc/hook/wiki_preprocess.bb @@ -0,0 +1,11 @@ +[h3]wiki_preprocess[/h3] + +Called before markdown/bbcode processors are run for wiki pages + +Passed parameter array: + + 'content' => wiki page content + 'mimetype' => page mimetype + + +see: Zotlabs/Module/Wiki.php diff --git a/doc/hooklist.bb b/doc/hooklist.bb index 34e19660e..d104df380 100644 --- a/doc/hooklist.bb +++ b/doc/hooklist.bb @@ -31,6 +31,15 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the [zrl=[baseurl]/help/hook/account_settings_post]account_settings_post[/zrl] Called when posting from the account settings form +[zrl=[baseurl]/help/hook/activity_filter]activity_filter[/zrl] + Called when generating the list of filters for the network page + +[zrl=[baseurl]/help/hook/activity_order]activity_order[/zrl] + Called when generating the list of order options for the network page + +[zrl=[baseurl]/help/hook/addon_app_installed_filter]addon_app_installed_filter[/zrl] + Called when determining whether an addon_app is installed + [zrl=[baseurl]/help/hook/activity_received]activity_received[/zrl] Called when an activity (post, comment, like, etc.) has been received from a zot source @@ -43,9 +52,18 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the [zrl=[baseurl]/help/hook/api_perm_is_allowed]api_perm_is_allowed[/zrl] Called when perm_is_allowed() is executed from an API call. +[zrl=[baseurl]/help/hook/app_destroy]app_destroy[/zrl] + Called when an app is deleted + +[zrl=[baseurl]/help/hook/app_installed_filter]app_installed_filter[/zrl] + Called when determining whether an app is installed + [zrl=[baseurl]/help/hook/app_menu]app_menu[/zrl] Called when generating the app_menu dropdown (may be obsolete) +[zrl=[baseurl]/help/hook/attach_delete]attach_delete[/zrl] + Called when attachments are deleted from the attach table + [zrl=[baseurl]/help/hook/atom_author]atom_author[/zrl] Called when generating an author or owner element for an Atom ActivityStream feed @@ -107,7 +125,7 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the Validate the email provided in an account registration [zrl=[baseurl]/help/hook/check_account_invite]check_account_invite[/zrl] - Validate an invitation code when using site invitations + Validate an invitation code when using site invitations [zrl=[baseurl]/help/hook/check_account_password]check_account_password[/zrl] Used to provide policy control over account passwords (minimum length, character set inclusion, etc.) @@ -131,7 +149,7 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the Called when posting to the features/addon settings page [zrl=[baseurl]/help/hook/construct_page]construct_page[/zrl] - General purpose hook to provide content to certain page regions. Called when constructing the Comanche page. + General purpose hook to provide content to certain page regions. Called when constructing the Comanche page. [zrl=[baseurl]/help/hook/contact_block_end]contact_block_end[/zrl] Called when generating the sidebar "Connections" widget @@ -145,8 +163,11 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the [zrl=[baseurl]/help/hook/contact_select_options]contact_select_options[/zrl] Deprecated/unused +[zrl=[baseurl]/help/hook/content_security_policy]content_security_policy[/zrl] + Called prior to output of the Content-Security-Policy header + [zrl=[baseurl]/help/hook/conversation_start]conversation_start[/zrl] - Called in the beginning of rendering a conversation (message or message collection or stream) + Called in the beginning of rendering a conversation (message or message collection or stream) [zrl=[baseurl]/help/hook/cover_photo_content_end]cover_photo_content_end[/zrl] Called after a cover photo has been uplaoded @@ -177,7 +198,7 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the [zrl=[baseurl]/help/hook/display_item]display_item[/zrl] Called for each item being displayed in a conversation thread - + [zrl=[baseurl]/help/hook/display_settings]display_settings[/zrl] Called from settings module when displaying the 'display settings' section @@ -196,6 +217,12 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the [zrl=[baseurl]/help/hook/dreport_is_storable]dreport_is_storable[/zrl] called before storing a dreport record to determine whether to store it +[zrl=[baseurl]/help/hook/dreport_process]dreport_process[/zrl] + called for each valid delivery report + +[zrl=[baseurl]/help/hook/dropdown_extras]dropdown_extras[/zrl] + Add additional items to the dropdown cog when item/threads are displayed. + [zrl=[baseurl]/help/hook/drop_item]drop_item[/zrl] called when an 'item' is removed @@ -254,7 +281,7 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the called to generate the HTML for displaying a map location by text location [zrl=[baseurl]/help/hook/get_all_api_perms]get_all_api_perms[/zrl] - Called when retrieving the permissions for API uses + Called when retrieving the permissions for API uses [zrl=[baseurl]/help/hook/get_all_perms]get_all_perms[/zrl] called when get_all_perms() is used @@ -394,9 +421,6 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the [zrl=[baseurl]/help/hook/network_ping]network_ping[/zrl] Called during a ping request -[zrl=[baseurl]/help/hook/network_tabs]network_tabs[/zrl] - Called when generating the list of tabs for the network page - [zrl=[baseurl]/help/hook/network_to_name]network_to_name[/zrl] Deprecated @@ -436,6 +460,9 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the [zrl=[baseurl]/help/hook/page_header]page_header[/zrl] Called when generating the navigation bar +[zrl=[baseurl]/help/hook/page_header]page_meta[/zrl] + Called when generating the meta data in the page header. + [zrl=[baseurl]/help/hook/parse_atom]parse_atom[/zrl] Called when parsing an atom/RSS feed item @@ -443,7 +470,7 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the Called when probing a URL to generate post content from it [zrl=[baseurl]/help/hook/pdl_selector]pdl_selector[/zrl] - Called when creating a layout selection in a form + Called when creating a layout selection in a form [zrl=[baseurl]/help/hook/perm_is_allowed]perm_is_allowed[/zrl] Called during perm_is_allowed() to determine if a permission is allowed for this channel and observer @@ -454,6 +481,9 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the [zrl=[baseurl]/help/hook/permissions_update]permissions_update[/zrl] Called when a permissions refresh is transmitted +[zrl=[baseurl]/help/hook/permit_hook]permit_hook[/zrl] + Called before a registered hook is actually executed to determine if it should be allowed or blocked + [zrl=[baseurl]/help/hook/personal_xrd]personal_xrd[/zrl] Called when generating the personal XRD for "old webfinger" (Diaspora) @@ -535,12 +565,9 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the [zrl=[baseurl]/help/hook/profile_sidebar_enter]profile_sidebar_enter[/zrl] Called before generating the 'channel sidebar' or mini-profile -[zrl=[baseurl]/help/hook/profile_tabs]profile_tabs[/zrl] - Called when generating the tabs for channel related pages (channel,profile,files,etc.) - [zrl=[baseurl]/help/hook/queue_deliver]queue_deliver[/zrl] Called when delivering a queued message - + [zrl=[baseurl]/help/hook/register_account]register_account[/zrl] Called when an account has been created @@ -571,9 +598,15 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the [zrl=[baseurl]/help/hook/smilie]smilie[/zrl] Called when translating emoticons +[zrl=[baseurl]/help/hook/status_editor]status_editor[/zrl] + Called when generating the status_editor. + [zrl=[baseurl]/help/hook/stream_item]stream_item[/zrl] Called for each item which is rendered for viewing via conversation() +[zrl=[baseurl]/help/hook/system_app_installed_filter]system_app_installed_filter[/zrl] + Called when determining whether a system app is installed + [zrl=[baseurl]/help/hook/tagged]tagged[/zrl] Called when a delivery is processed which results in you being tagged @@ -592,6 +625,9 @@ Hooks allow plugins/addons to "hook into" the code at many points and alter the [zrl=[baseurl]/help/hook/well_known]well_known[/zrl] Called when accessing the '.well-known' special site addresses +[zrl=[baseurl]/help/hook/wiki_preprocess]wiki_preprocess[/zrl] + Called before markdown/bbcode processors are run for wiki pages + [zrl=[baseurl]/help/hook/zot_best_algorithm]zot_best_algorithm[/zrl] Called when negotiating crypto algorithms with remote sites diff --git a/doc/hooks.html b/doc/hooks.html index 6009127f6..a7ee314e7 100644 --- a/doc/hooks.html +++ b/doc/hooks.html @@ -1 +1 @@ -
Function | Source File | Arg |
$a->module . _mod_aftercontent | index.php | $arr |
$a->module . _mod_content | index.php | $arr |
$a->module . _mod_init | index.php | $placeholder |
$a->module . _mod_post | index.php | $_POST |
$a->module . _post_ . $selname | include/acl_selectors.php | $o |
$a->module . _post_ . $selname | include/acl_selectors.php | $o |
$a->module . _post_ . $selname | include/acl_selectors.php | $o |
$a->module . _pre_ . $selname | include/acl_selectors.php | $arr |
$a->module . _pre_ . $selname | include/acl_selectors.php | $arr |
$a->module . _pre_ . $selname | include/acl_selectors.php | $arr |
$name | include/plugin.php | &$data = null |
about_hook | mod/siteinfo.php | $o |
accept_follow | mod/connedit.php | $arr |
account_downgrade | include/account.php | $ret |
account_downgrade | include/account.php | $ret |
account_settings | mod/settings.php | $account_settings |
activity_received | include/zot.php | $parr |
affinity_labels | include/widgets.php | $labels |
affinity_labels | mod/connedit.php | $labels |
api_perm_is_allowed | include/permissions.php | $arr |
app_menu | index.php | $arr |
atom_author | include/items.php | $o |
atom_entry | include/items.php | $o |
atom_feed | include/items.php | $atom |
atom_feed_end | include/items.php | $atom |
attach_upload_file | include/attach.php | $f |
authenticate | include/auth.php | $addon_auth |
avatar_lookup | include/network.php | $avatar |
bb2diaspora | include/markdown.php | $Text |
bbcode | include/bbcode.php | $Text |
channel_remove | include/Contact.php | $r[0] |
chat_message | include/chat.php | $arr |
chat_post | mod/chatsvc.php | $arr |
check_account_email | include/account.php | $arr |
check_account_invite | include/account.php | $arr |
check_account_password | include/account.php | $arr |
connect_premium | mod/connect.php | $arr |
connector_settings | mod/settings.php | $settings_connectors |
construct_page | boot.php | $arr |
contact_block_end | include/text.php | $arr |
contact_edit | mod/connedit.php | $arr |
contact_edit_post | mod/connedit.php | $_POST |
contact_select_options | include/acl_selectors.php | $x |
conversation_start | include/conversation.php | $cb |
create_identity | include/channel.php | $newuid |
cron | include/cronhooks.php | $d |
cron_daily | include/poller.php | datetime_convert() |
cron_weekly | include/poller.php | datetime_convert() |
directory_item | mod/directory.php | $arr |
discover_by_webbie | include/network.php | $arr |
display_item | include/ItemObject.php | $arr |
display_item | include/conversation.php | $arr |
display_settings | mod/settings.php | $o |
display_settings_post | mod/settings.php | $_POST |
donate_contributors | extend/addon/matrix/donate/donate.php | $contributors |
donate_plugin | extend/addon/matrix/donate/donate.php | $o |
donate_sponsors | extend/addon/matrix/donate/donate.php | $sponsors |
dreport_is_storable | include/zot.php | $dr |
drop_item | include/items.php | $arr |
enotify | include/enotify.php | $h |
enotify_mail | include/enotify.php | $datarray |
enotify_store | include/enotify.php | $datarray |
event_created | include/event.php | $event[id] |
event_updated | include/event.php | $event[id] |
externals_url_select | include/externals.php | $arr |
feature_enabled | include/features.php | $arr |
feature_settings | mod/settings.php | $settings_addons |
feature_settings_post | mod/settings.php | $_POST |
follow | include/follow.php | $arr |
follow | include/follow.php | $arr |
follow_allow | include/follow.php | $x |
gender_selector | include/profile_selectors.php | $select |
gender_selector_min | include/profile_selectors.php | $select |
generate_map | include/text.php | $arr |
generate_named_map | include/text.php | $arr |
get_all_api_perms | include/permissions.php | $arr |
get_all_perms | include/permissions.php | $arr |
get_features | include/features.php | $arr |
get_role_perms | include/permissions.php | $ret |
get_widgets | boot.php | $arr |
get_widgets | boot.php | $arr |
global_permissions | include/permissions.php | $ret |
home_content | mod/home.php | $o |
home_init | mod/home.php | $ret |
hostxrd | mod/hostxrd.php | $arr |
html2bbcode | include/html2bbcode.php | $message |
identity_basic_export | include/channel.php | $addon |
import_author_xchan | include/items.php | $arr |
import_channel | mod/import.php | $addon |
import_directory_profile | include/zot.php | $d |
import_xchan | include/zot.php | $arr |
item_photo_menu | include/conversation.php | $args |
item_store | include/items.php | $d |
item_store | include/items.php | $arr |
item_store_update | include/items.php | $d |
item_translate | include/items.php | $translate |
item_translate | include/items.php | $translate |
jot_networks | include/acl_selectors.php | $jotnets |
jot_networks | include/conversation.php | $jotnets |
jot_networks | mod/editblock.php | $jotnets |
jot_networks | mod/editpost.php | $jotnets |
jot_networks | mod/editwebpage.php | $jotnets |
jot_networks | mod/editlayout.php | $jotnets |
jot_tool | include/conversation.php | $jotplugins |
jot_tool | mod/editblock.php | $jotplugins |
jot_tool | mod/editpost.php | $jotplugins |
jot_tool | mod/editwebpage.php | $jotplugins |
jot_tool | mod/editlayout.php | $jotplugins |
load_pdl | boot.php | $arr |
local_dir_update | include/dir_fns.php | $arr |
logged_in | include/oauth.php | $a->user |
logged_in | include/api.php | $a->user |
logged_in | include/security.php | $a->account |
logged_in | include/security.php | $user_record |
logging_out | include/auth.php | $args |
login_hook | boot.php | $o |
magic_auth | mod/magic.php | $arr |
magic_auth_openid_success | mod/openid.php | $arr |
magic_auth_openid_success | mod/openid.php | $arr |
magic_auth_success | mod/post.php | $arr |
main_slider | include/widgets.php | $arr |
marital_selector | include/profile_selectors.php | $select |
marital_selector_min | include/profile_selectors.php | $select |
module_loaded | index.php | $x |
mood_verbs | include/text.php | $arr |
nav | include/nav.php | $x |
network_content_init | mod/network.php | $arr |
network_ping | mod/ping.php | $arr |
network_tabs | include/conversation.php | $arr |
network_to_name | include/contact_selectors.php | $nets |
notifier_end | include/notifier.php | $target_item |
notifier_hub | include/notifier.php | $narr |
notifier_normal | include/deliver_hooks.php | $r[0] |
obj_verbs | include/taxonomy.php | $arr |
oembed_probe | include/oembed.php | $x |
page_content_top | index.php | $a->page[content] |
page_end | index.php | $a->page[content] |
page_header | include/nav.php | $a->page[nav] |
parse_atom | include/items.php | $arr |
parse_link | mod/linkinfo.php | $arr |
pdl_selector | include/comanche.php | $arr |
perm_is_allowed | include/permissions.php | $arr |
permissions_create | include/notifier.php | $perm_update |
permissions_update | include/notifier.php | $perm_update |
personal_xrd | mod/xrd.php | $arr |
photo_post_end | include/photos.php | $ret |
photo_post_end | include/photos.php | $ret |
photo_upload_begin | include/attach.php | $arr |
photo_upload_begin | include/photos.php | $args |
photo_upload_end | include/attach.php | $ret |
photo_upload_end | include/attach.php | $ret |
photo_upload_end | include/attach.php | $ret |
photo_upload_end | include/attach.php | $ret |
photo_upload_end | include/attach.php | $ret |
photo_upload_end | include/photos.php | $ret |
photo_upload_end | include/photos.php | $ret |
photo_upload_end | include/photos.php | $ret |
photo_upload_end | include/photos.php | $ret |
photo_upload_file | include/attach.php | $f |
photo_upload_file | include/photos.php | $f |
photo_upload_form | mod/photos.php | $ret |
poke_verbs | include/text.php | $arr |
post_local | include/zot.php | $arr |
post_local | include/items.php | $arr |
post_local | mod/item.php | $datarray |
post_local_end | include/items.php | $arr |
post_local_end | include/attach.php | $arr |
post_local_end | include/attach.php | $arr |
post_local_end | extend/addon/matrix/randpost/randpost.php | $x |
post_local_end | extend/addon/matrix/randpost/randpost.php | $x |
post_local_end | mod/mood.php | $arr |
post_local_end | mod/like.php | $arr |
post_local_end | mod/item.php | $datarray |
post_local_end | mod/subthread.php | $arr |
post_local_start | mod/item.php | $_REQUEST |
post_mail | include/items.php | $arr |
post_mail_end | include/items.php | $arr |
post_remote | include/items.php | $arr |
post_remote_end | include/items.php | $arr |
post_remote_update | include/items.php | $arr |
post_remote_update_end | include/items.php | $arr |
prepare_body | include/text.php | $prep_arr |
prepare_body_final | include/text.php | $prep_arr |
prepare_body_init | include/text.php | $item |
probe_well_known | include/probe.php | $ret |
proc_run | boot.php | $arr |
process_channel_sync_delivery | include/zot.php | $addon |
profile_advanced | mod/profile.php | $o |
profile_edit | mod/profiles.php | $arr |
profile_photo_content_end | mod/profile_photo.php | $o |
profile_post | mod/profiles.php | $_POST |
profile_sidebar | include/channel.php | $arr |
profile_sidebar_enter | include/channel.php | $profile |
profile_tabs | include/conversation.php | $arr |
register_account | include/account.php | $result |
render_location | include/conversation.php | $locate |
replace_macros | include/text.php | $arr |
reverse_magic_auth | mod/rmagic.php | $arr |
settings_account | mod/settings.php | $_POST |
settings_form | mod/settings.php | $o |
settings_post | mod/settings.php | $_POST |
sexpref_selector | include/profile_selectors.php | $select |
sexpref_selector_min | include/profile_selectors.php | $select |
smilie | include/text.php | $params |
smilie | extend/addon/matrix/smileybutton/smileybutton.php | $params |
tagged | include/items.php | $arr |
validate_channelname | include/channel.php | $arr |
webfinger | mod/wfinger.php | $arr |
well_known | mod/_well_known.php | $arr |
zid | include/channel.php | $arr |
zid_init | include/channel.php | $arr |
zot_finger | include/zot.php | $ret |
Generated Tue Nov 03 21:19:02 PST 2015
Function | Source File | Arg |
$a->module . _mod_aftercontent | index.php | $arr |
$a->module . _mod_content | index.php | $arr |
$a->module . _mod_init | index.php | $placeholder |
$a->module . _mod_post | index.php | $_POST |
$a->module . _post_ . $selname | include/acl_selectors.php | $o |
$a->module . _post_ . $selname | include/acl_selectors.php | $o |
$a->module . _post_ . $selname | include/acl_selectors.php | $o |
$a->module . _pre_ . $selname | include/acl_selectors.php | $arr |
$a->module . _pre_ . $selname | include/acl_selectors.php | $arr |
$a->module . _pre_ . $selname | include/acl_selectors.php | $arr |
$name | include/plugin.php | &$data = null |
about_hook | mod/siteinfo.php | $o |
accept_follow | mod/connedit.php | $arr |
account_downgrade | include/account.php | $ret |
account_downgrade | include/account.php | $ret |
account_settings | mod/settings.php | $account_settings |
activity_received | include/zot.php | $parr |
affinity_labels | include/widgets.php | $labels |
affinity_labels | mod/connedit.php | $labels |
api_perm_is_allowed | include/permissions.php | $arr |
app_menu | index.php | $arr |
atom_author | include/items.php | $o |
atom_entry | include/items.php | $o |
atom_feed | include/items.php | $atom |
atom_feed_end | include/items.php | $atom |
attach_upload_file | include/attach.php | $f |
authenticate | include/auth.php | $addon_auth |
avatar_lookup | include/network.php | $avatar |
bb2diaspora | include/markdown.php | $Text |
bbcode | include/bbcode.php | $Text |
channel_remove | include/Contact.php | $r[0] |
chat_message | include/chat.php | $arr |
chat_post | mod/chatsvc.php | $arr |
check_account_email | include/account.php | $arr |
check_account_invite | include/account.php | $arr |
check_account_password | include/account.php | $arr |
connect_premium | mod/connect.php | $arr |
connector_settings | mod/settings.php | $settings_connectors |
construct_page | boot.php | $arr |
contact_block_end | include/text.php | $arr |
contact_edit | mod/connedit.php | $arr |
contact_edit_post | mod/connedit.php | $_POST |
contact_select_options | include/acl_selectors.php | $x |
conversation_start | include/conversation.php | $cb |
create_identity | include/channel.php | $newuid |
cron | include/cronhooks.php | $d |
cron_daily | include/poller.php | datetime_convert() |
cron_weekly | include/poller.php | datetime_convert() |
directory_item | mod/directory.php | $arr |
discover_by_webbie | include/network.php | $arr |
display_item | include/ItemObject.php | $arr |
display_item | include/conversation.php | $arr |
display_settings | mod/settings.php | $o |
display_settings_post | mod/settings.php | $_POST |
donate_contributors | extend/addon/matrix/donate/donate.php | $contributors |
donate_plugin | extend/addon/matrix/donate/donate.php | $o |
donate_sponsors | extend/addon/matrix/donate/donate.php | $sponsors |
dreport_is_storable | include/zot.php | $dr |
drop_item | include/items.php | $arr |
enotify | include/enotify.php | $h |
enotify_mail | include/enotify.php | $datarray |
enotify_store | include/enotify.php | $datarray |
event_created | include/event.php | $event[id] |
event_updated | include/event.php | $event[id] |
externals_url_select | include/externals.php | $arr |
feature_enabled | include/features.php | $arr |
feature_settings | mod/settings.php | $settings_addons |
feature_settings_post | mod/settings.php | $_POST |
follow | include/follow.php | $arr |
follow | include/follow.php | $arr |
follow_allow | include/follow.php | $x |
gender_selector | include/profile_selectors.php | $select |
gender_selector_min | include/profile_selectors.php | $select |
generate_map | include/text.php | $arr |
generate_named_map | include/text.php | $arr |
get_all_api_perms | include/permissions.php | $arr |
get_all_perms | include/permissions.php | $arr |
get_features | include/features.php | $arr |
get_role_perms | include/permissions.php | $ret |
get_widgets | boot.php | $arr |
get_widgets | boot.php | $arr |
global_permissions | include/permissions.php | $ret |
home_content | mod/home.php | $o |
home_init | mod/home.php | $ret |
hostxrd | mod/hostxrd.php | $arr |
html2bbcode | include/html2bbcode.php | $message |
identity_basic_export | include/channel.php | $addon |
import_author_xchan | include/items.php | $arr |
import_channel | mod/import.php | $addon |
import_directory_profile | include/zot.php | $d |
import_xchan | include/zot.php | $arr |
item_photo_menu | include/conversation.php | $args |
item_store | include/items.php | $d |
item_store | include/items.php | $arr |
item_store_update | include/items.php | $d |
item_translate | include/items.php | $translate |
item_translate | include/items.php | $translate |
jot_networks | include/acl_selectors.php | $jotnets |
jot_networks | include/conversation.php | $jotnets |
jot_networks | mod/editblock.php | $jotnets |
jot_networks | mod/editpost.php | $jotnets |
jot_networks | mod/editwebpage.php | $jotnets |
jot_networks | mod/editlayout.php | $jotnets |
jot_tool | include/conversation.php | $jotplugins |
jot_tool | mod/editblock.php | $jotplugins |
jot_tool | mod/editpost.php | $jotplugins |
jot_tool | mod/editwebpage.php | $jotplugins |
jot_tool | mod/editlayout.php | $jotplugins |
load_pdl | boot.php | $arr |
local_dir_update | include/dir_fns.php | $arr |
logged_in | include/oauth.php | $a->user |
logged_in | include/api.php | $a->user |
logged_in | include/security.php | $a->account |
logged_in | include/security.php | $user_record |
logging_out | include/auth.php | $args |
login_hook | boot.php | $o |
magic_auth | mod/magic.php | $arr |
magic_auth_openid_success | mod/openid.php | $arr |
magic_auth_openid_success | mod/openid.php | $arr |
magic_auth_success | mod/post.php | $arr |
main_slider | include/widgets.php | $arr |
marital_selector | include/profile_selectors.php | $select |
marital_selector_min | include/profile_selectors.php | $select |
module_loaded | index.php | $x |
mood_verbs | include/text.php | $arr |
nav | include/nav.php | $x |
network_content_init | mod/network.php | $arr |
network_ping | mod/ping.php | $arr |
network_tabs | include/conversation.php | $arr |
network_to_name | include/contact_selectors.php | $nets |
notifier_end | include/notifier.php | $target_item |
notifier_hub | include/notifier.php | $narr |
notifier_normal | include/deliver_hooks.php | $r[0] |
obj_verbs | include/taxonomy.php | $arr |
oembed_probe | include/oembed.php | $x |
page_content_top | index.php | $a->page[content] |
page_end | index.php | $a->page[content] |
page_header | include/nav.php | $a->page[nav] |
parse_atom | include/items.php | $arr |
parse_link | mod/linkinfo.php | $arr |
pdl_selector | include/comanche.php | $arr |
perm_is_allowed | include/permissions.php | $arr |
permissions_create | include/notifier.php | $perm_update |
permissions_update | include/notifier.php | $perm_update |
personal_xrd | mod/xrd.php | $arr |
photo_post_end | include/photos.php | $ret |
photo_post_end | include/photos.php | $ret |
photo_upload_begin | include/attach.php | $arr |
photo_upload_begin | include/photos.php | $args |
photo_upload_end | include/attach.php | $ret |
photo_upload_end | include/attach.php | $ret |
photo_upload_end | include/attach.php | $ret |
photo_upload_end | include/attach.php | $ret |
photo_upload_end | include/attach.php | $ret |
photo_upload_end | include/photos.php | $ret |
photo_upload_end | include/photos.php | $ret |
photo_upload_end | include/photos.php | $ret |
photo_upload_end | include/photos.php | $ret |
photo_upload_file | include/attach.php | $f |
photo_upload_file | include/photos.php | $f |
photo_upload_form | mod/photos.php | $ret |
poke_verbs | include/text.php | $arr |
post_local | include/zot.php | $arr |
post_local | include/items.php | $arr |
post_local | mod/item.php | $datarray |
post_local_end | include/items.php | $arr |
post_local_end | include/attach.php | $arr |
post_local_end | include/attach.php | $arr |
post_local_end | extend/addon/matrix/randpost/randpost.php | $x |
post_local_end | extend/addon/matrix/randpost/randpost.php | $x |
post_local_end | mod/mood.php | $arr |
post_local_end | mod/like.php | $arr |
post_local_end | mod/item.php | $datarray |
post_local_end | mod/subthread.php | $arr |
post_local_start | mod/item.php | $_REQUEST |
post_mail | include/items.php | $arr |
post_mail_end | include/items.php | $arr |
post_remote | include/items.php | $arr |
post_remote_end | include/items.php | $arr |
post_remote_update | include/items.php | $arr |
post_remote_update_end | include/items.php | $arr |
prepare_body | include/text.php | $prep_arr |
prepare_body_final | include/text.php | $prep_arr |
prepare_body_init | include/text.php | $item |
probe_well_known | include/probe.php | $ret |
proc_run | boot.php | $arr |
process_channel_sync_delivery | include/zot.php | $addon |
profile_advanced | mod/profile.php | $o |
profile_edit | mod/profiles.php | $arr |
profile_photo_content_end | mod/profile_photo.php | $o |
profile_post | mod/profiles.php | $_POST |
profile_sidebar | include/channel.php | $arr |
profile_sidebar_enter | include/channel.php | $profile |
register_account | include/account.php | $result |
render_location | include/conversation.php | $locate |
replace_macros | include/text.php | $arr |
reverse_magic_auth | mod/rmagic.php | $arr |
settings_account | mod/settings.php | $_POST |
settings_form | mod/settings.php | $o |
settings_post | mod/settings.php | $_POST |
sexpref_selector | include/profile_selectors.php | $select |
sexpref_selector_min | include/profile_selectors.php | $select |
smilie | include/text.php | $params |
smilie | extend/addon/matrix/smileybutton/smileybutton.php | $params |
tagged | include/items.php | $arr |
validate_channelname | include/channel.php | $arr |
webfinger | mod/wfinger.php | $arr |
well_known | mod/_well_known.php | $arr |
zid | include/channel.php | $arr |
zid_init | include/channel.php | $arr |
zot_finger | include/zot.php | $ret |
Generated Tue Nov 03 21:19:02 PST 2015
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\n// stylelint-disable font-weight-notation\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n// stylelint-enable font-weight-notation\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\n// stylelint-disable font-family-no-duplicate-names\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; // Correct the inheritance and scaling of font size in all browsers.\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n// stylelint-enable font-family-no-duplicate-names\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so\n // we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\n// stylelint-disable font-weight-notation\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n// stylelint-enable font-weight-notation\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\n// stylelint-disable font-family-no-duplicate-names\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; // Correct the inheritance and scaling of font size in all browsers.\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n// stylelint-enable font-family-no-duplicate-names\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so\n // we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\n// stylelint-disable font-weight-notation\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n// stylelint-enable font-weight-notation\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\n// stylelint-disable font-family-no-duplicate-names\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; // Correct the inheritance and scaling of font size in all browsers.\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n// stylelint-enable font-family-no-duplicate-names\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so\n // we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `