2019-10-03 11:20:36 +02:00
< ? php
/**
* @ file src / Model / GServer . php
* @ brief This file includes the GServer class to handle with servers
*/
namespace Friendica\Model ;
use DOMDocument ;
use DOMXPath ;
use Friendica\Core\Config ;
use Friendica\Core\Protocol ;
use Friendica\Database\DBA ;
use Friendica\Module\Register ;
use Friendica\Util\Network ;
use Friendica\Util\DateTimeFormat ;
use Friendica\Util\Strings ;
use Friendica\Util\XML ;
use Friendica\Core\Logger ;
2019-10-04 01:33:41 +02:00
use Friendica\Protocol\PortableContact ;
use Friendica\Protocol\Diaspora ;
2019-10-03 11:20:36 +02:00
/**
* @ brief This class handles GServer related functions
*/
class GServer
{
2019-10-04 01:33:41 +02:00
public static function check ( $server_url , $force = false )
{
// Unify the server address
$server_url = trim ( $server_url , '/' );
$server_url = str_replace ( '/index.php' , '' , $server_url );
if ( $server_url == '' ) {
return false ;
}
$gserver = DBA :: selectFirst ( 'gserver' , [], [ 'nurl' => Strings :: normaliseLink ( $server_url )]);
if ( DBA :: isResult ( $gserver )) {
if ( $gserver [ 'created' ] <= DBA :: NULL_DATETIME ) {
$fields = [ 'created' => DateTimeFormat :: utcNow ()];
$condition = [ 'nurl' => Strings :: normaliseLink ( $server_url )];
DBA :: update ( 'gserver' , $fields , $condition );
}
$last_contact = $gserver [ " last_contact " ];
$last_failure = $gserver [ " last_failure " ];
// See discussion under https://forum.friendi.ca/display/0b6b25a8135aabc37a5a0f5684081633
// It can happen that a zero date is in the database, but storing it again is forbidden.
if ( $last_contact < DBA :: NULL_DATETIME ) {
$last_contact = DBA :: NULL_DATETIME ;
}
if ( $last_failure < DBA :: NULL_DATETIME ) {
$last_failure = DBA :: NULL_DATETIME ;
}
if ( ! $force && ! PortableContact :: updateNeeded ( $gserver [ 'created' ], '' , $last_failure , $last_contact )) {
Logger :: info ( 'Use cached data' , [ 'server' => $server_url ]);
return ( $last_contact >= $last_failure );
}
}
Logger :: info ( 'Server is outdated or unknown. Start discovery.' , [ 'Server' => $server_url , 'Force' => $force , 'Created' => $gserver [ 'created' ], 'Failure' => $last_failure , 'Contact' => $last_contact ]);
return self :: detect ( $server_url );
}
2019-10-03 11:20:36 +02:00
/**
2019-10-03 16:48:46 +02:00
* Detect server data ( type , protocol , version number , ... )
* The detected data is then updated or inserted in the gserver table .
2019-10-03 11:20:36 +02:00
*
2019-10-03 17:30:07 +02:00
* @ param string $url Server url
2019-10-03 11:20:36 +02:00
*
* @ return boolean 'true' if server could be detected
*/
2019-10-03 16:48:46 +02:00
public static function detect ( $url )
2019-10-03 11:20:36 +02:00
{
$serverdata = [];
// When a nodeinfo is present, we don't need to dig further
$xrd_timeout = Config :: get ( 'system' , 'xrd_timeout' );
$curlResult = Network :: curl ( $url . '/.well-known/nodeinfo' , false , [ 'timeout' => $xrd_timeout ]);
if ( $curlResult -> isTimeout ()) {
DBA :: update ( 'gserver' , [ 'last_failure' => DateTimeFormat :: utcNow ()], [ 'nurl' => Strings :: normaliseLink ( $url )]);
return false ;
}
$nodeinfo = self :: fetchNodeinfo ( $url , $curlResult );
2019-10-03 16:48:46 +02:00
// When nodeinfo isn't present, we use the older 'statistics.json' endpoint
2019-10-03 15:02:48 +02:00
if ( empty ( $nodeinfo )) {
$nodeinfo = self :: fetchStatistics ( $url );
}
2019-10-03 16:48:46 +02:00
// If that didn't work out well, we use some protocol specific endpoints
2019-10-03 22:52:04 +02:00
if ( empty ( $nodeinfo ) || empty ( $nodeinfo [ 'network' ]) || ( $nodeinfo [ 'network' ] == Protocol :: DFRN )) {
2019-10-03 11:20:36 +02:00
// Fetch the landing page, possibly it reveals some data
$curlResult = Network :: curl ( $url , false , [ 'timeout' => $xrd_timeout ]);
if ( $curlResult -> isSuccess ()) {
$serverdata = self :: analyseRootHeader ( $curlResult , $serverdata );
$serverdata = self :: analyseRootBody ( $curlResult , $serverdata );
}
if ( ! $curlResult -> isSuccess () || empty ( $curlResult -> getBody ())) {
DBA :: update ( 'gserver' , [ 'last_failure' => DateTimeFormat :: utcNow ()], [ 'nurl' => Strings :: normaliseLink ( $url )]);
return false ;
}
if ( empty ( $serverdata [ 'network' ]) || ( $serverdata [ 'network' ] == Protocol :: DFRN )) {
$serverdata = self :: detectFriendica ( $url , $serverdata );
}
if ( empty ( $serverdata [ 'network' ]) || ( $serverdata [ 'network' ] == Protocol :: ACTIVITYPUB )) {
$serverdata = self :: detectMastodonAlikes ( $url , $serverdata );
}
2019-10-03 15:02:48 +02:00
// the 'siteinfo.json' is some specific endpoint of Hubzilla and Red
2019-10-03 12:44:29 +02:00
if ( empty ( $serverdata [ 'network' ]) || ( $serverdata [ 'network' ] == Protocol :: ZOT )) {
2019-10-03 11:20:36 +02:00
$serverdata = self :: fetchSiteinfo ( $url , $serverdata );
}
2019-10-03 16:48:46 +02:00
// The 'siteinfo.json' doesn't seem to be present on older Hubzilla installations
2019-10-03 15:02:48 +02:00
if ( empty ( $serverdata [ 'network' ])) {
$serverdata = self :: detectHubzilla ( $url , $serverdata );
}
2019-10-03 11:20:36 +02:00
if ( empty ( $serverdata [ 'network' ])) {
$serverdata = self :: detectNextcloud ( $url , $serverdata );
}
if ( empty ( $serverdata [ 'network' ])) {
$serverdata = self :: detectGNUSocial ( $url , $serverdata );
}
} else {
$serverdata = $nodeinfo ;
}
2019-10-03 17:30:07 +02:00
$serverdata = self :: checkPoCo ( $url , $serverdata );
2019-10-03 11:20:36 +02:00
// We can't detect the network type. Possibly it is some system that we don't know yet
if ( empty ( $serverdata [ 'network' ])) {
$serverdata [ 'network' ] = Protocol :: PHANTOM ;
}
2019-10-03 22:52:04 +02:00
// Check host-meta for phantom networks.
// Although this is not needed, it is a good indicator for a living system,
// since most systems had implemented it.
if (( $serverdata [ 'network' ] == Protocol :: PHANTOM ) && ! self :: validHostMeta ( $url )) {
DBA :: update ( 'gserver' , [ 'last_failure' => DateTimeFormat :: utcNow ()], [ 'nurl' => Strings :: normaliseLink ( $url )]);
return false ;
}
2019-10-03 11:20:36 +02:00
$serverdata [ 'url' ] = $url ;
$serverdata [ 'nurl' ] = Strings :: normaliseLink ( $url );
2019-10-03 17:30:07 +02:00
// We take the highest number that we do find
$registeredUsers = $serverdata [ 'registered-users' ] ? ? 0 ;
// On an active server there has to be at least a single user
if (( $serverdata [ 'network' ] != Protocol :: PHANTOM ) && ( $registeredUsers == 0 )) {
$registeredUsers = 1 ;
2019-10-03 12:44:29 +02:00
}
2019-10-03 17:30:07 +02:00
$gcontacts = DBA :: count ( 'gcontact' , [ 'server_url' => [ $url , $serverdata [ 'nurl' ]]]);
$apcontacts = DBA :: count ( 'apcontact' , [ 'baseurl' => [ $url , $serverdata [ 'nurl' ]]]);
$contacts = DBA :: count ( 'contact' , [ 'uid' => 0 , 'baseurl' => [ $url , $serverdata [ 'nurl' ]]]);
$serverdata [ 'registered-users' ] = max ( $gcontacts , $apcontacts , $contacts , $registeredUsers );
2019-10-03 16:48:46 +02:00
$serverdata [ 'last_contact' ] = DateTimeFormat :: utcNow ();
if ( ! DBA :: exists ( 'gserver' , [ 'nurl' => Strings :: normaliseLink ( $url )])) {
2019-10-03 11:20:36 +02:00
$serverdata [ 'created' ] = DateTimeFormat :: utcNow ();
2019-10-03 16:48:46 +02:00
$ret = DBA :: insert ( 'gserver' , $serverdata );
2019-10-03 11:20:36 +02:00
} else {
2019-10-03 16:48:46 +02:00
$ret = DBA :: update ( 'gserver' , $serverdata , [ 'nurl' => $serverdata [ 'nurl' ]]);
2019-10-03 11:20:36 +02:00
}
2019-10-04 01:33:41 +02:00
if ( in_array ( $serverdata [ 'network' ], [ Protocol :: DFRN , Protocol :: DIASPORA ])) {
self :: discoverRelay ( $url );
}
2019-10-03 16:48:46 +02:00
return $ret ;
2019-10-03 11:20:36 +02:00
}
2019-10-04 01:33:41 +02:00
/**
* @ brief Fetch relay data from a given server url
*
* @ param string $server_url address of the server
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function discoverRelay ( $server_url )
{
Logger :: info ( 'Discover relay data' , [ 'server' => $server_url ]);
$curlResult = Network :: curl ( $server_url . '/.well-known/x-social-relay' );
if ( ! $curlResult -> isSuccess ()) {
return ;
}
$data = json_decode ( $curlResult -> getBody (), true );
if ( ! is_array ( $data )) {
return ;
}
$gserver = DBA :: selectFirst ( 'gserver' , [ 'id' , 'relay-subscribe' , 'relay-scope' ], [ 'nurl' => Strings :: normaliseLink ( $server_url )]);
if ( ! DBA :: isResult ( $gserver )) {
return ;
}
if (( $gserver [ 'relay-subscribe' ] != $data [ 'subscribe' ]) || ( $gserver [ 'relay-scope' ] != $data [ 'scope' ])) {
$fields = [ 'relay-subscribe' => $data [ 'subscribe' ], 'relay-scope' => $data [ 'scope' ]];
DBA :: update ( 'gserver' , $fields , [ 'id' => $gserver [ 'id' ]]);
}
DBA :: delete ( 'gserver-tag' , [ 'gserver-id' => $gserver [ 'id' ]]);
if ( $data [ 'scope' ] == 'tags' ) {
// Avoid duplicates
$tags = [];
foreach ( $data [ 'tags' ] as $tag ) {
$tag = mb_strtolower ( $tag );
if ( strlen ( $tag ) < 100 ) {
$tags [ $tag ] = $tag ;
}
}
foreach ( $tags as $tag ) {
DBA :: insert ( 'gserver-tag' , [ 'gserver-id' => $gserver [ 'id' ], 'tag' => $tag ], true );
}
}
// Create or update the relay contact
$fields = [];
if ( isset ( $data [ 'protocols' ])) {
if ( isset ( $data [ 'protocols' ][ 'diaspora' ])) {
$fields [ 'network' ] = Protocol :: DIASPORA ;
if ( isset ( $data [ 'protocols' ][ 'diaspora' ][ 'receive' ])) {
$fields [ 'batch' ] = $data [ 'protocols' ][ 'diaspora' ][ 'receive' ];
} elseif ( is_string ( $data [ 'protocols' ][ 'diaspora' ])) {
$fields [ 'batch' ] = $data [ 'protocols' ][ 'diaspora' ];
}
}
if ( isset ( $data [ 'protocols' ][ 'dfrn' ])) {
$fields [ 'network' ] = Protocol :: DFRN ;
if ( isset ( $data [ 'protocols' ][ 'dfrn' ][ 'receive' ])) {
$fields [ 'batch' ] = $data [ 'protocols' ][ 'dfrn' ][ 'receive' ];
} elseif ( is_string ( $data [ 'protocols' ][ 'dfrn' ])) {
$fields [ 'batch' ] = $data [ 'protocols' ][ 'dfrn' ];
}
}
}
Diaspora :: setRelayContact ( $server_url , $fields );
}
2019-10-03 15:02:48 +02:00
private static function fetchStatistics ( $url )
{
$curlResult = Network :: curl ( $url . '/statistics.json' );
if ( ! $curlResult -> isSuccess ()) {
return [];
}
$data = json_decode ( $curlResult -> getBody (), true );
if ( empty ( $data )) {
return [];
}
$serverdata = [];
if ( ! empty ( $data [ 'version' ])) {
$serverdata [ 'version' ] = $data [ 'version' ];
// Version numbers on statistics.json are presented with additional info, e.g.:
// 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
$serverdata [ 'version' ] = preg_replace ( '=(.+)-(.{4,})=ism' , '$1' , $serverdata [ 'version' ]);
}
if ( ! empty ( $data [ 'name' ])) {
$serverdata [ 'site_name' ] = $data [ 'name' ];
}
if ( ! empty ( $data [ 'network' ])) {
$serverdata [ 'platform' ] = $data [ 'network' ];
if ( $serverdata [ 'platform' ] == 'Diaspora' ) {
$serverdata [ 'network' ] = Protocol :: DIASPORA ;
} elseif ( $serverdata [ 'platform' ] == 'Friendica' ) {
$serverdata [ 'network' ] = Protocol :: DFRN ;
2019-10-03 16:48:46 +02:00
} elseif ( $serverdata [ 'platform' ] == 'hubzilla' ) {
$serverdata [ 'network' ] = Protocol :: ZOT ;
2019-10-03 15:02:48 +02:00
} elseif ( $serverdata [ 'platform' ] == 'redmatrix' ) {
$serverdata [ 'network' ] = Protocol :: ZOT ;
}
}
if ( ! empty ( $data [ 'registrations_open' ])) {
$serverdata [ 'register_policy' ] = Register :: OPEN ;
} else {
$serverdata [ 'register_policy' ] = Register :: CLOSED ;
}
return $serverdata ;
}
2019-10-03 11:20:36 +02:00
/**
* @ brief Detect server type by using the nodeinfo data
*
* @ param string $url address of the server
* @ return array Server data
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function fetchNodeinfo ( $url , $curlResult )
{
$nodeinfo = json_decode ( $curlResult -> getBody (), true );
2019-10-03 16:48:46 +02:00
if ( ! is_array ( $nodeinfo ) || empty ( $nodeinfo [ 'links' ])) {
2019-10-03 11:20:36 +02:00
return [];
}
$nodeinfo1_url = '' ;
$nodeinfo2_url = '' ;
foreach ( $nodeinfo [ 'links' ] as $link ) {
if ( ! is_array ( $link ) || empty ( $link [ 'rel' ]) || empty ( $link [ 'href' ])) {
Logger :: info ( 'Invalid nodeinfo format' , [ 'url' => $url ]);
continue ;
}
if ( $link [ 'rel' ] == 'http://nodeinfo.diaspora.software/ns/schema/1.0' ) {
$nodeinfo1_url = $link [ 'href' ];
} elseif ( $link [ 'rel' ] == 'http://nodeinfo.diaspora.software/ns/schema/2.0' ) {
$nodeinfo2_url = $link [ 'href' ];
}
}
if ( $nodeinfo1_url . $nodeinfo2_url == '' ) {
return [];
}
$server = [];
// When the nodeinfo url isn't on the same host, then there is obviously something wrong
if ( ! empty ( $nodeinfo2_url ) && ( parse_url ( $url , PHP_URL_HOST ) == parse_url ( $nodeinfo2_url , PHP_URL_HOST ))) {
$server = self :: parseNodeinfo2 ( $nodeinfo2_url );
}
// When the nodeinfo url isn't on the same host, then there is obviously something wrong
if ( empty ( $server ) && ! empty ( $nodeinfo1_url ) && ( parse_url ( $url , PHP_URL_HOST ) == parse_url ( $nodeinfo1_url , PHP_URL_HOST ))) {
$server = self :: parseNodeinfo1 ( $nodeinfo1_url );
}
return $server ;
}
/**
* @ brief Parses Nodeinfo 1
*
* @ param string $nodeinfo_url address of the nodeinfo path
* @ return array Server data
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function parseNodeinfo1 ( $nodeinfo_url )
{
$curlResult = Network :: curl ( $nodeinfo_url );
if ( ! $curlResult -> isSuccess ()) {
return false ;
}
$nodeinfo = json_decode ( $curlResult -> getBody (), true );
if ( ! is_array ( $nodeinfo )) {
return false ;
}
$server = [];
$server [ 'register_policy' ] = Register :: CLOSED ;
if ( ! empty ( $nodeinfo [ 'openRegistrations' ])) {
$server [ 'register_policy' ] = Register :: OPEN ;
}
if ( is_array ( $nodeinfo [ 'software' ])) {
2019-10-03 16:48:46 +02:00
if ( ! empty ( $nodeinfo [ 'software' ][ 'name' ])) {
2019-10-03 11:20:36 +02:00
$server [ 'platform' ] = $nodeinfo [ 'software' ][ 'name' ];
}
2019-10-03 16:48:46 +02:00
if ( ! empty ( $nodeinfo [ 'software' ][ 'version' ])) {
2019-10-03 11:20:36 +02:00
$server [ 'version' ] = $nodeinfo [ 'software' ][ 'version' ];
// Version numbers on Nodeinfo are presented with additional info, e.g.:
// 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
$server [ 'version' ] = preg_replace ( '=(.+)-(.{4,})=ism' , '$1' , $server [ 'version' ]);
}
}
2019-10-03 16:48:46 +02:00
if ( ! empty ( $nodeinfo [ 'metadata' ][ 'nodeName' ])) {
2019-10-03 11:20:36 +02:00
$server [ 'site_name' ] = $nodeinfo [ 'metadata' ][ 'nodeName' ];
}
if ( ! empty ( $nodeinfo [ 'usage' ][ 'users' ][ 'total' ])) {
$server [ 'registered-users' ] = $nodeinfo [ 'usage' ][ 'users' ][ 'total' ];
}
if ( ! empty ( $nodeinfo [ 'protocols' ][ 'inbound' ]) && is_array ( $nodeinfo [ 'protocols' ][ 'inbound' ])) {
$protocols = [];
foreach ( $nodeinfo [ 'protocols' ][ 'inbound' ] as $protocol ) {
$protocols [ $protocol ] = true ;
}
if ( ! empty ( $protocols [ 'friendica' ])) {
$server [ 'network' ] = Protocol :: DFRN ;
} elseif ( ! empty ( $protocols [ 'activitypub' ])) {
$server [ 'network' ] = Protocol :: ACTIVITYPUB ;
} elseif ( ! empty ( $protocols [ 'diaspora' ])) {
$server [ 'network' ] = Protocol :: DIASPORA ;
} elseif ( ! empty ( $protocols [ 'ostatus' ])) {
$server [ 'network' ] = Protocol :: OSTATUS ;
} elseif ( ! empty ( $protocols [ 'gnusocial' ])) {
$server [ 'network' ] = Protocol :: OSTATUS ;
2019-10-03 15:02:48 +02:00
} elseif ( ! empty ( $protocols [ 'zot' ])) {
$server [ 'network' ] = Protocol :: ZOT ;
2019-10-03 11:20:36 +02:00
}
}
if ( ! $server ) {
return false ;
}
return $server ;
}
/**
* @ brief Parses Nodeinfo 2
*
* @ param string $nodeinfo_url address of the nodeinfo path
* @ return array Server data
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function parseNodeinfo2 ( $nodeinfo_url )
{
$curlResult = Network :: curl ( $nodeinfo_url );
if ( ! $curlResult -> isSuccess ()) {
return false ;
}
$nodeinfo = json_decode ( $curlResult -> getBody (), true );
if ( ! is_array ( $nodeinfo )) {
return false ;
}
$server = [];
$server [ 'register_policy' ] = Register :: CLOSED ;
if ( ! empty ( $nodeinfo [ 'openRegistrations' ])) {
$server [ 'register_policy' ] = Register :: OPEN ;
}
if ( is_array ( $nodeinfo [ 'software' ])) {
2019-10-03 16:48:46 +02:00
if ( ! empty ( $nodeinfo [ 'software' ][ 'name' ])) {
2019-10-03 11:20:36 +02:00
$server [ 'platform' ] = $nodeinfo [ 'software' ][ 'name' ];
}
2019-10-03 16:48:46 +02:00
if ( ! empty ( $nodeinfo [ 'software' ][ 'version' ])) {
2019-10-03 11:20:36 +02:00
$server [ 'version' ] = $nodeinfo [ 'software' ][ 'version' ];
// Version numbers on Nodeinfo are presented with additional info, e.g.:
// 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
$server [ 'version' ] = preg_replace ( '=(.+)-(.{4,})=ism' , '$1' , $server [ 'version' ]);
}
}
2019-10-03 16:48:46 +02:00
if ( ! empty ( $nodeinfo [ 'metadata' ][ 'nodeName' ])) {
2019-10-03 11:20:36 +02:00
$server [ 'site_name' ] = $nodeinfo [ 'metadata' ][ 'nodeName' ];
}
if ( ! empty ( $nodeinfo [ 'usage' ][ 'users' ][ 'total' ])) {
$server [ 'registered-users' ] = $nodeinfo [ 'usage' ][ 'users' ][ 'total' ];
}
if ( ! empty ( $nodeinfo [ 'protocols' ])) {
$protocols = [];
foreach ( $nodeinfo [ 'protocols' ] as $protocol ) {
$protocols [ $protocol ] = true ;
}
if ( ! empty ( $protocols [ 'friendica' ])) {
$server [ 'network' ] = Protocol :: DFRN ;
} elseif ( ! empty ( $protocols [ 'activitypub' ])) {
$server [ 'network' ] = Protocol :: ACTIVITYPUB ;
} elseif ( ! empty ( $protocols [ 'diaspora' ])) {
$server [ 'network' ] = Protocol :: DIASPORA ;
} elseif ( ! empty ( $protocols [ 'ostatus' ])) {
$server [ 'network' ] = Protocol :: OSTATUS ;
} elseif ( ! empty ( $protocols [ 'gnusocial' ])) {
$server [ 'network' ] = Protocol :: OSTATUS ;
2019-10-03 15:02:48 +02:00
} elseif ( ! empty ( $protocols [ 'zot' ])) {
$server [ 'network' ] = Protocol :: ZOT ;
2019-10-03 11:20:36 +02:00
}
}
if ( empty ( $server )) {
return false ;
}
return $server ;
}
private static function fetchSiteinfo ( $url , $serverdata )
{
$curlResult = Network :: curl ( $url . '/siteinfo.json' );
if ( ! $curlResult -> isSuccess ()) {
return $serverdata ;
}
$data = json_decode ( $curlResult -> getBody (), true );
if ( empty ( $data )) {
return $serverdata ;
}
2019-10-03 16:48:46 +02:00
if ( ! empty ( $data [ 'url' ])) {
2019-10-03 11:20:36 +02:00
$serverdata [ 'platform' ] = $data [ 'platform' ];
$serverdata [ 'version' ] = $data [ 'version' ];
}
if ( ! empty ( $data [ 'plugins' ])) {
if ( in_array ( 'pubcrawl' , $data [ 'plugins' ])) {
$serverdata [ 'network' ] = Protocol :: ACTIVITYPUB ;
} elseif ( in_array ( 'diaspora' , $data [ 'plugins' ])) {
$serverdata [ 'network' ] = Protocol :: DIASPORA ;
} elseif ( in_array ( 'gnusoc' , $data [ 'plugins' ])) {
$serverdata [ 'network' ] = Protocol :: OSTATUS ;
} else {
$serverdata [ 'network' ] = Protocol :: ZOT ;
}
}
if ( ! empty ( $data [ 'site_name' ])) {
$serverdata [ 'site_name' ] = $data [ 'site_name' ];
}
if ( ! empty ( $data [ 'channels_total' ])) {
$serverdata [ 'registered-users' ] = $data [ 'channels_total' ];
}
if ( ! empty ( $data [ 'register_policy' ])) {
switch ( $data [ 'register_policy' ]) {
2019-10-03 15:02:48 +02:00
case 'REGISTER_OPEN' :
2019-10-03 11:20:36 +02:00
$serverdata [ 'register_policy' ] = Register :: OPEN ;
break ;
2019-10-03 15:02:48 +02:00
case 'REGISTER_APPROVE' :
2019-10-03 11:20:36 +02:00
$serverdata [ 'register_policy' ] = Register :: APPROVE ;
break ;
2019-10-03 15:02:48 +02:00
case 'REGISTER_CLOSED' :
2019-10-03 11:20:36 +02:00
default :
$serverdata [ 'register_policy' ] = Register :: CLOSED ;
break ;
}
}
return $serverdata ;
}
2019-10-03 22:52:04 +02:00
private static function validHostMeta ( $url )
{
$xrd_timeout = Config :: get ( 'system' , 'xrd_timeout' );
$curlResult = Network :: curl ( $url . '/.well-known/host-meta' , false , [ 'timeout' => $xrd_timeout ]);
if ( ! $curlResult -> isSuccess ()) {
return false ;
}
$xrd = XML :: parseString ( $curlResult -> getBody (), false );
if ( ! is_object ( $xrd )) {
return false ;
}
$elements = XML :: elementToArray ( $xrd );
if ( empty ( $elements ) || empty ( $elements [ 'xrd' ]) || empty ( $elements [ 'xrd' ][ 'link' ])) {
return false ;
}
$valid = false ;
foreach ( $elements [ 'xrd' ][ 'link' ] as $link ) {
if ( empty ( $link [ 'rel' ]) || empty ( $link [ 'type' ]) || empty ( $link [ 'template' ])) {
continue ;
}
if ( $link [ 'type' ] == 'application/xrd+xml' ) {
// When the webfinger host is the same like the system host, it should be ok.
$valid = ( parse_url ( $url , PHP_URL_HOST ) == parse_url ( $link [ 'template' ], PHP_URL_HOST ));
}
}
return $valid ;
}
2019-10-03 17:30:07 +02:00
private static function checkPoCo ( $url , $serverdata )
{
$curlResult = Network :: curl ( $url . '/poco' );
if ( ! $curlResult -> isSuccess ()) {
return $serverdata ;
}
$data = json_decode ( $curlResult -> getBody (), true );
if ( empty ( $data )) {
return $serverdata ;
}
if ( ! empty ( $data [ 'totalResults' ])) {
$registeredUsers = $serverdata [ 'registered-users' ] ? ? 0 ;
$serverdata [ 'registered-users' ] = max ( $data [ 'totalResults' ], $registeredUsers );
$serverdata [ 'poco' ] = $url . '/poco' ;
} else {
$serverdata [ 'poco' ] = '' ;
}
return $serverdata ;
}
2019-10-03 11:20:36 +02:00
private static function detectNextcloud ( $url , $serverdata )
{
$curlResult = Network :: curl ( $url . '/status.php' );
if ( ! $curlResult -> isSuccess () || ( $curlResult -> getBody () == '' )) {
return $serverdata ;
}
$data = json_decode ( $curlResult -> getBody (), true );
if ( empty ( $data )) {
return $serverdata ;
}
if ( ! empty ( $data [ 'version' ])) {
$serverdata [ 'platform' ] = 'nextcloud' ;
$serverdata [ 'version' ] = $data [ 'version' ];
$serverdata [ 'network' ] = Protocol :: ACTIVITYPUB ;
}
return $serverdata ;
}
private static function detectMastodonAlikes ( $url , $serverdata )
{
$curlResult = Network :: curl ( $url . '/api/v1/instance' );
if ( ! $curlResult -> isSuccess () || ( $curlResult -> getBody () == '' )) {
return $serverdata ;
}
$data = json_decode ( $curlResult -> getBody (), true );
if ( empty ( $data )) {
return $serverdata ;
}
2019-10-03 15:02:48 +02:00
if ( ! empty ( $data [ 'version' ])) {
2019-10-03 11:20:36 +02:00
$serverdata [ 'platform' ] = 'mastodon' ;
$serverdata [ 'version' ] = defaults ( $data , 'version' , '' );
$serverdata [ 'network' ] = Protocol :: ACTIVITYPUB ;
}
2019-10-03 15:02:48 +02:00
if ( ! empty ( $data [ 'title' ])) {
$serverdata [ 'site_name' ] = $data [ 'title' ];
}
if ( ! empty ( $data [ 'description' ])) {
$serverdata [ 'info' ] = trim ( $data [ 'description' ]);
}
2019-10-03 11:20:36 +02:00
if ( ! empty ( $data [ 'stats' ][ 'user_count' ])) {
$serverdata [ 'registered-users' ] = $data [ 'stats' ][ 'user_count' ];
}
2019-10-03 22:52:04 +02:00
if ( ! empty ( $serverdata [ 'version' ]) && preg_match ( '/.*?\(compatible;\s(.*)\s(.*)\)/ism' , $serverdata [ 'version' ], $matches )) {
$serverdata [ 'platform' ] = $matches [ 1 ];
$serverdata [ 'version' ] = $matches [ 2 ];
}
2019-10-03 15:02:48 +02:00
if ( ! empty ( $serverdata [ 'version' ]) && strstr ( $serverdata [ 'version' ], 'Pleroma' )) {
2019-10-03 11:20:36 +02:00
$serverdata [ 'platform' ] = 'pleroma' ;
2019-10-03 22:52:04 +02:00
$serverdata [ 'version' ] = trim ( str_replace ( 'Pleroma' , '' , $serverdata [ 'version' ]));
2019-10-03 11:20:36 +02:00
}
return $serverdata ;
}
2019-10-03 15:02:48 +02:00
private static function detectHubzilla ( $url , $serverdata )
{
$curlResult = Network :: curl ( $url . '/api/statusnet/config.json' );
if ( ! $curlResult -> isSuccess () || ( $curlResult -> getBody () == '' )) {
return $serverdata ;
}
$data = json_decode ( $curlResult -> getBody (), true );
if ( empty ( $data )) {
return $serverdata ;
}
if ( ! empty ( $data [ 'site' ][ 'name' ])) {
$serverdata [ 'site_name' ] = $data [ 'site' ][ 'name' ];
}
if ( ! empty ( $data [ 'site' ][ 'platform' ])) {
$serverdata [ 'platform' ] = $data [ 'site' ][ 'platform' ][ 'PLATFORM_NAME' ];
$serverdata [ 'version' ] = $data [ 'site' ][ 'platform' ][ 'STD_VERSION' ];
$serverdata [ 'network' ] = Protocol :: ZOT ;
}
2019-10-03 16:48:46 +02:00
if ( ! empty ( $data [ 'site' ][ 'hubzilla' ])) {
2019-10-03 15:02:48 +02:00
$serverdata [ 'platform' ] = $data [ 'site' ][ 'hubzilla' ][ 'PLATFORM_NAME' ];
$serverdata [ 'version' ] = $data [ 'site' ][ 'hubzilla' ][ 'RED_VERSION' ];
$serverdata [ 'network' ] = Protocol :: ZOT ;
}
2019-10-03 16:48:46 +02:00
if ( ! empty ( $data [ 'site' ][ 'redmatrix' ])) {
if ( ! empty ( $data [ 'site' ][ 'redmatrix' ][ 'PLATFORM_NAME' ])) {
2019-10-03 15:02:48 +02:00
$serverdata [ 'platform' ] = $data [ 'site' ][ 'redmatrix' ][ 'PLATFORM_NAME' ];
2019-10-03 16:48:46 +02:00
} elseif ( ! empty ( $data [ 'site' ][ 'redmatrix' ][ 'RED_PLATFORM' ])) {
2019-10-03 15:02:48 +02:00
$serverdata [ 'platform' ] = $data [ 'site' ][ 'redmatrix' ][ 'RED_PLATFORM' ];
}
$serverdata [ 'version' ] = $data [ 'site' ][ 'redmatrix' ][ 'RED_VERSION' ];
$serverdata [ 'network' ] = Protocol :: ZOT ;
}
$private = false ;
$inviteonly = false ;
$closed = false ;
if ( ! empty ( $data [ 'site' ][ 'closed' ])) {
$closed = self :: toBoolean ( $data [ 'site' ][ 'closed' ]);
}
if ( ! empty ( $data [ 'site' ][ 'private' ])) {
$private = self :: toBoolean ( $data [ 'site' ][ 'private' ]);
}
if ( ! empty ( $data [ 'site' ][ 'inviteonly' ])) {
$inviteonly = self :: toBoolean ( $data [ 'site' ][ 'inviteonly' ]);
}
if ( ! $closed && ! $private and $inviteonly ) {
$register_policy = Register :: APPROVE ;
} elseif ( ! $closed && ! $private ) {
$register_policy = Register :: OPEN ;
} else {
$register_policy = Register :: CLOSED ;
}
return $serverdata ;
}
private static function toBoolean ( $val )
{
if (( $val == 'true' ) || ( $val == 1 )) {
return true ;
} elseif (( $val == 'false' ) || ( $val == 0 )) {
return false ;
}
return $val ;
}
2019-10-03 11:20:36 +02:00
private static function detectGNUSocial ( $url , $serverdata )
{
$curlResult = Network :: curl ( $url . '/api/statusnet/version.json' );
if ( $curlResult -> isSuccess () && ( $curlResult -> getBody () != '{"error":"not implemented"}' ) &&
( $curlResult -> getBody () != '' ) && ( strlen ( $curlResult -> getBody ()) < 30 )) {
$serverdata [ 'platform' ] = 'StatusNet' ;
// Remove junk that some GNU Social servers return
$serverdata [ 'version' ] = str_replace ( chr ( 239 ) . chr ( 187 ) . chr ( 191 ), '' , $curlResult -> getBody ());
$serverdata [ 'version' ] = trim ( $serverdata [ 'version' ], '"' );
$serverdata [ 'network' ] = Protocol :: OSTATUS ;
}
// Test for GNU Social
$curlResult = Network :: curl ( $url . '/api/gnusocial/version.json' );
if ( $curlResult -> isSuccess () && ( $curlResult -> getBody () != '{"error":"not implemented"}' ) &&
( $curlResult -> getBody () != '' ) && ( strlen ( $curlResult -> getBody ()) < 30 )) {
$serverdata [ 'platform' ] = 'GNU Social' ;
// Remove junk that some GNU Social servers return
$serverdata [ 'version' ] = str_replace ( chr ( 239 ) . chr ( 187 ) . chr ( 191 ), '' , $curlResult -> getBody ());
$serverdata [ 'version' ] = trim ( $serverdata [ 'version' ], '"' );
$serverdata [ 'network' ] = Protocol :: OSTATUS ;
}
return $serverdata ;
}
private static function detectFriendica ( $url , $serverdata )
{
$curlResult = Network :: curl ( $url . '/friendica/json' );
if ( ! $curlResult -> isSuccess ()) {
$curlResult = Network :: curl ( $url . '/friendika/json' );
}
if ( ! $curlResult -> isSuccess ()) {
return $serverdata ;
}
$data = json_decode ( $curlResult -> getBody (), true );
if ( empty ( $data ) || empty ( $data [ 'version' ])) {
return $serverdata ;
}
$serverdata [ 'network' ] = Protocol :: DFRN ;
$serverdata [ 'version' ] = $data [ 'version' ];
if ( ! empty ( $data [ 'no_scrape_url' ])) {
$serverdata [ 'noscrape' ] = $data [ 'no_scrape_url' ];
}
if ( ! empty ( $data [ 'site_name' ])) {
$serverdata [ 'site_name' ] = $data [ 'site_name' ];
}
if ( ! empty ( $data [ 'info' ])) {
$serverdata [ 'info' ] = trim ( $data [ 'info' ]);
}
$register_policy = defaults ( $data , 'register_policy' , 'REGISTER_CLOSED' );
switch ( $register_policy ) {
case 'REGISTER_OPEN' :
$serverdata [ 'register_policy' ] = Register :: OPEN ;
break ;
case 'REGISTER_APPROVE' :
$serverdata [ 'register_policy' ] = Register :: APPROVE ;
break ;
case 'REGISTER_CLOSED' :
case 'REGISTER_INVITATION' :
$serverdata [ 'register_policy' ] = Register :: CLOSED ;
break ;
default :
Logger :: info ( 'Register policy is invalid' , [ 'policy' => $register_policy , 'server' => $url ]);
$serverdata [ 'register_policy' ] = Register :: CLOSED ;
break ;
}
$serverdata [ 'platform' ] = defaults ( $data , 'platform' , '' );
return $serverdata ;
}
private static function analyseRootBody ( $curlResult , $serverdata )
{
$doc = new DOMDocument ();
@ $doc -> loadHTML ( $curlResult -> getBody ());
$xpath = new DOMXPath ( $doc );
$title = trim ( XML :: getFirstNodeValue ( $xpath , '//head/title/text()' ));
if ( ! empty ( $title )) {
$serverdata [ 'site_name' ] = $title ;
}
$list = $xpath -> query ( '//meta[@name]' );
foreach ( $list as $node ) {
$attr = [];
if ( $node -> attributes -> length ) {
foreach ( $node -> attributes as $attribute ) {
$attribute -> value = trim ( $attribute -> value );
if ( empty ( $attribute -> value )) {
continue ;
}
$attr [ $attribute -> name ] = $attribute -> value ;
}
2019-10-03 12:44:29 +02:00
if ( empty ( $attr [ 'name' ]) || empty ( $attr [ 'content' ])) {
continue ;
}
2019-10-03 11:20:36 +02:00
}
2019-10-03 23:39:48 +02:00
2019-10-03 11:20:36 +02:00
if ( $attr [ 'name' ] == 'description' ) {
$serverdata [ 'info' ] = $attr [ 'content' ];
}
if ( $attr [ 'name' ] == 'application-name' ) {
$serverdata [ 'platform' ] = $attr [ 'content' ];
if ( in_array ( $attr [ 'content' ], [ 'Misskey' , 'Write.as' ])) {
$serverdata [ 'network' ] = Protocol :: ACTIVITYPUB ;
}
}
2019-10-03 16:48:46 +02:00
if ( $attr [ 'name' ] == 'generator' ) {
2019-10-03 11:20:36 +02:00
$serverdata [ 'platform' ] = $attr [ 'content' ];
$version_part = explode ( ' ' , $attr [ 'content' ]);
2019-10-03 16:48:46 +02:00
if ( count ( $version_part ) == 2 ) {
2019-10-03 11:20:36 +02:00
if ( in_array ( $version_part [ 0 ], [ 'WordPress' ])) {
$serverdata [ 'platform' ] = $version_part [ 0 ];
$serverdata [ 'version' ] = $version_part [ 1 ];
$serverdata [ 'network' ] = Protocol :: ACTIVITYPUB ;
}
if ( in_array ( $version_part [ 0 ], [ 'Friendika' , 'Friendica' ])) {
$serverdata [ 'platform' ] = $version_part [ 0 ];
$serverdata [ 'version' ] = $version_part [ 1 ];
$serverdata [ 'network' ] = Protocol :: DFRN ;
}
}
}
}
$list = $xpath -> query ( '//meta[@property]' );
foreach ( $list as $node ) {
$attr = [];
if ( $node -> attributes -> length ) {
foreach ( $node -> attributes as $attribute ) {
$attribute -> value = trim ( $attribute -> value );
if ( empty ( $attribute -> value )) {
continue ;
}
$attr [ $attribute -> name ] = $attribute -> value ;
}
2019-10-03 12:44:29 +02:00
if ( empty ( $attr [ 'property' ]) || empty ( $attr [ 'content' ])) {
continue ;
}
2019-10-03 11:20:36 +02:00
}
if ( $attr [ 'property' ] == 'og:site_name' ) {
$serverdata [ 'site_name' ] = $attr [ 'content' ];
}
if ( $attr [ 'property' ] == 'og:description' ) {
$serverdata [ 'info' ] = $attr [ 'content' ];
}
2019-10-03 12:44:29 +02:00
if ( $attr [ 'property' ] == 'og:platform' ) {
$serverdata [ 'platform' ] = $attr [ 'content' ];
if ( in_array ( $attr [ 'content' ], [ 'PeerTube' ])) {
$serverdata [ 'network' ] = Protocol :: ACTIVITYPUB ;
}
2019-10-03 11:20:36 +02:00
}
2019-10-03 12:44:29 +02:00
if ( $attr [ 'property' ] == 'generator' ) {
2019-10-03 11:20:36 +02:00
$serverdata [ 'platform' ] = $attr [ 'content' ];
2019-10-03 12:44:29 +02:00
if ( in_array ( $attr [ 'content' ], [ 'hubzilla' ])) {
// We later check which compatible protocol modules are loaded.
$serverdata [ 'network' ] = Protocol :: ZOT ;
}
2019-10-03 11:20:36 +02:00
}
}
return $serverdata ;
}
private static function analyseRootHeader ( $curlResult , $serverdata )
{
if ( $curlResult -> getHeader ( 'server' ) == 'Mastodon' ) {
$serverdata [ 'platform' ] = 'mastodon' ;
$serverdata [ 'network' ] = $network = Protocol :: ACTIVITYPUB ;
} elseif ( $curlResult -> inHeader ( 'x-diaspora-version' )) {
$serverdata [ 'platform' ] = 'diaspora' ;
$serverdata [ 'network' ] = $network = Protocol :: DIASPORA ;
$serverdata [ 'version' ] = $curlResult -> getHeader ( 'x-diaspora-version' );
} elseif ( $curlResult -> inHeader ( 'x-friendica-version' )) {
$serverdata [ 'platform' ] = 'friendica' ;
$serverdata [ 'network' ] = $network = Protocol :: DFRN ;
$serverdata [ 'version' ] = $curlResult -> getHeader ( 'x-friendica-version' );
}
return $serverdata ;
}
}