2019-10-03 11:20:36 +02:00
< ? php
/**
2020-02-09 15:45:36 +01:00
* @ copyright Copyright ( C ) 2020 , Friendica
*
* @ license GNU AGPL version 3 or any later version
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*
2019-10-03 11:20:36 +02:00
*/
2020-02-09 15:45:36 +01:00
2019-10-03 11:20:36 +02:00
namespace Friendica\Model ;
use DOMDocument ;
use DOMXPath ;
use Friendica\Core\Protocol ;
2020-01-05 03:19:02 +01:00
use Friendica\Core\Worker ;
2019-10-03 11:20:36 +02:00
use Friendica\Database\DBA ;
2020-01-19 21:26:42 +01:00
use Friendica\DI ;
2019-10-03 11:20:36 +02:00
use Friendica\Module\Register ;
2019-12-30 03:51:16 +01:00
use Friendica\Network\CurlResult ;
2019-10-03 11:20:36 +02:00
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-04 08:33:16 +02:00
use Friendica\Network\Probe ;
2019-10-03 11:20:36 +02:00
/**
2019-10-04 19:29:21 +02:00
* This class handles GServer related functions
2019-10-03 11:20:36 +02:00
*/
class GServer
{
2019-12-21 14:48:20 +01:00
// Directory types
const DT_NONE = 0 ;
const DT_POCO = 1 ;
const DT_MASTODON = 2 ;
2019-10-06 01:30:47 +02:00
/**
* Checks if the given server is reachable
*
* @ param string $profile URL of the given profile
* @ param string $server URL of the given server ( If empty , taken from profile )
* @ param string $network Network value that is used , when detection failed
* @ param boolean $force Force an update .
*
* @ return boolean 'true' if server seems vital
*/
public static function reachable ( string $profile , string $server = '' , string $network = '' , bool $force = false )
{
if ( $server == '' ) {
2020-01-01 18:54:36 +01:00
$server = GContact :: getBasepath ( $profile );
2019-10-06 01:30:47 +02:00
}
if ( $server == '' ) {
return true ;
}
return self :: check ( $server , $network , $force );
}
2019-12-21 07:39:22 +01:00
/**
* Decides if a server needs to be updated , based upon several date fields
2019-12-21 14:48:20 +01:00
*
2019-12-21 07:39:22 +01:00
* @ param date $created Creation date of that server entry
* @ param date $updated When had the server entry be updated
* @ param date $last_failure Last failure when contacting that server
* @ param date $last_contact Last time the server had been contacted
2019-12-21 14:48:20 +01:00
*
2019-12-21 07:39:22 +01:00
* @ return boolean Does the server record needs an update ?
*/
public static function updateNeeded ( $created , $updated , $last_failure , $last_contact )
{
$now = strtotime ( DateTimeFormat :: utcNow ());
if ( $updated > $last_contact ) {
$contact_time = strtotime ( $updated );
} else {
$contact_time = strtotime ( $last_contact );
}
$failure_time = strtotime ( $last_failure );
$created_time = strtotime ( $created );
// If there is no "created" time then use the current time
if ( $created_time <= 0 ) {
$created_time = $now ;
}
// If the last contact was less than 24 hours then don't update
if (( $now - $contact_time ) < ( 60 * 60 * 24 )) {
return false ;
}
// If the last failure was less than 24 hours then don't update
if (( $now - $failure_time ) < ( 60 * 60 * 24 )) {
return false ;
}
// If the last contact was less than a week ago and the last failure is older than a week then don't update
//if ((($now - $contact_time) < (60 * 60 * 24 * 7)) && ($contact_time > $failure_time))
// return false;
// If the last contact time was more than a week ago and the contact was created more than a week ago, then only try once a week
if ((( $now - $contact_time ) > ( 60 * 60 * 24 * 7 )) && (( $now - $created_time ) > ( 60 * 60 * 24 * 7 )) && (( $now - $failure_time ) < ( 60 * 60 * 24 * 7 ))) {
return false ;
}
// If the last contact time was more than a month ago and the contact was created more than a month ago, then only try once a month
if ((( $now - $contact_time ) > ( 60 * 60 * 24 * 30 )) && (( $now - $created_time ) > ( 60 * 60 * 24 * 30 )) && (( $now - $failure_time ) < ( 60 * 60 * 24 * 30 ))) {
return false ;
}
return true ;
}
2019-10-04 19:29:21 +02:00
/**
* Checks the state of the given server .
*
* @ param string $server_url URL of the given server
* @ param string $network Network value that is used , when detection failed
* @ param boolean $force Force an update .
*
2019-10-05 06:22:16 +02:00
* @ return boolean 'true' if server seems vital
2019-10-04 19:29:21 +02:00
*/
public static function check ( string $server_url , string $network = '' , bool $force = false )
2019-10-04 01:33:41 +02:00
{
// 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 );
}
2019-10-05 06:22:16 +02:00
$last_contact = $gserver [ 'last_contact' ];
$last_failure = $gserver [ 'last_failure' ];
2019-10-04 01:33:41 +02:00
// 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 ;
}
2019-12-21 07:39:22 +01:00
if ( ! $force && ! self :: updateNeeded ( $gserver [ 'created' ], '' , $last_failure , $last_contact )) {
2019-10-04 07:42:54 +02:00
Logger :: info ( 'No update needed' , [ 'server' => $server_url ]);
2019-10-04 01:33:41 +02:00
return ( $last_contact >= $last_failure );
}
2019-10-04 07:42:54 +02:00
Logger :: info ( 'Server is outdated. Start discovery.' , [ 'Server' => $server_url , 'Force' => $force , 'Created' => $gserver [ 'created' ], 'Failure' => $last_failure , 'Contact' => $last_contact ]);
} else {
Logger :: info ( 'Server is unknown. Start discovery.' , [ 'Server' => $server_url ]);
2019-10-04 01:33:41 +02:00
}
2019-10-04 07:42:54 +02:00
return self :: detect ( $server_url , $network );
2019-10-04 01:33:41 +02:00
}
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-04 19:29:21 +02:00
* @ param string $url URL of the given server
* @ param string $network Network value that is used , when detection failed
2019-10-03 11:20:36 +02:00
*
* @ return boolean 'true' if server could be detected
*/
2019-10-04 19:29:21 +02:00
public static function detect ( string $url , string $network = '' )
2019-10-03 11:20:36 +02:00
{
2019-12-21 14:48:20 +01:00
Logger :: info ( 'Detect server type' , [ 'server' => $url ]);
2019-10-03 11:20:36 +02:00
$serverdata = [];
2019-12-21 14:48:20 +01:00
$original_url = $url ;
// Remove URL content that is not supposed to exist for a server url
$urlparts = parse_url ( $url );
unset ( $urlparts [ 'user' ]);
unset ( $urlparts [ 'pass' ]);
unset ( $urlparts [ 'query' ]);
unset ( $urlparts [ 'fragment' ]);
$url = Network :: unparseURL ( $urlparts );
// If the URL missmatches, then we mark the old entry as failure
if ( $url != $original_url ) {
DBA :: update ( 'gserver' , [ 'last_failure' => DateTimeFormat :: utcNow ()], [ 'nurl' => Strings :: normaliseLink ( $original_url )]);
}
2019-10-03 11:20:36 +02:00
// When a nodeinfo is present, we don't need to dig further
2020-01-19 21:21:13 +01:00
$xrd_timeout = DI :: config () -> get ( 'system' , 'xrd_timeout' );
2019-10-03 11:20:36 +02:00
$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-06 18:33:39 +02:00
// For Friendica and Zot based networks we have to dive deeper to reveal more details
if ( empty ( $nodeinfo [ 'network' ]) || in_array ( $nodeinfo [ 'network' ], [ Protocol :: DFRN , Protocol :: ZOT ])) {
2019-10-03 11:20:36 +02:00
// Fetch the landing page, possibly it reveals some data
2019-10-06 18:33:39 +02:00
if ( empty ( $nodeinfo [ 'network' ])) {
$curlResult = Network :: curl ( $url , false , [ 'timeout' => $xrd_timeout ]);
if ( $curlResult -> isSuccess ()) {
$serverdata = self :: analyseRootHeader ( $curlResult , $serverdata );
$serverdata = self :: analyseRootBody ( $curlResult , $serverdata , $url );
}
2020-01-12 13:50:00 +01:00
if ( ! $curlResult -> isSuccess () || empty ( $curlResult -> getBody ()) || self :: invalidBody ( $curlResult -> getBody ())) {
2019-10-06 18:33:39 +02:00
DBA :: update ( 'gserver' , [ 'last_failure' => DateTimeFormat :: utcNow ()], [ 'nurl' => Strings :: normaliseLink ( $url )]);
return false ;
}
}
if ( empty ( $serverdata [ 'network' ]) || ( $serverdata [ 'network' ] == Protocol :: ACTIVITYPUB )) {
$serverdata = self :: detectMastodonAlikes ( $url , $serverdata );
2019-10-03 11:20:36 +02:00
}
2019-10-06 18:33:39 +02:00
// All following checks are done for systems that always have got a "host-meta" endpoint.
// With this check we don't have to waste time and ressources for dead systems.
// Also this hopefully prevents us from receiving abuse messages.
if ( empty ( $serverdata [ 'network' ]) && ! self :: validHostMeta ( $url )) {
2019-10-03 11:20:36 +02:00
DBA :: update ( 'gserver' , [ 'last_failure' => DateTimeFormat :: utcNow ()], [ 'nurl' => Strings :: normaliseLink ( $url )]);
return false ;
}
2019-10-06 18:33:39 +02:00
if ( empty ( $serverdata [ 'network' ]) || in_array ( $serverdata [ 'network' ], [ Protocol :: DFRN , Protocol :: ACTIVITYPUB ])) {
2019-10-03 11:20:36 +02:00
$serverdata = self :: detectFriendica ( $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-12-21 14:48:20 +01:00
// Detect the directory type
$serverdata [ 'directory-type' ] = self :: DT_NONE ;
2019-10-03 17:30:07 +02:00
$serverdata = self :: checkPoCo ( $url , $serverdata );
2019-12-21 14:48:20 +01:00
$serverdata = self :: checkMastodonDirectory ( $url , $serverdata );
2019-10-03 17:30:07 +02:00
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-04 08:33:16 +02:00
// When we hadn't been able to detect the network type, we use the hint from the parameter
if (( $serverdata [ 'network' ] == Protocol :: PHANTOM ) && ! empty ( $network )) {
$serverdata [ 'network' ] = $network ;
}
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-04 08:33:16 +02:00
if ( $serverdata [ 'network' ] != Protocol :: PHANTOM ) {
$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 );
} else {
$serverdata [ 'registered-users' ] = $registeredUsers ;
$serverdata = self :: detectNetworkViaContacts ( $url , $serverdata );
}
2019-10-03 17:30:07 +02:00
2019-10-03 16:48:46 +02:00
$serverdata [ 'last_contact' ] = DateTimeFormat :: utcNow ();
2019-10-04 08:33:16 +02:00
$gserver = DBA :: selectFirst ( 'gserver' , [ 'network' ], [ 'nurl' => Strings :: normaliseLink ( $url )]);
if ( ! DBA :: isResult ( $gserver )) {
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-05 06:22:16 +02:00
// Don't override the network with 'unknown' when there had been a valid entry before
2019-10-04 08:33:16 +02:00
if (( $serverdata [ 'network' ] == Protocol :: PHANTOM ) && ! empty ( $gserver [ 'network' ])) {
unset ( $serverdata [ 'network' ]);
}
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 08:33:16 +02:00
if ( ! empty ( $serverdata [ 'network' ]) && in_array ( $serverdata [ 'network' ], [ Protocol :: DFRN , Protocol :: DIASPORA ])) {
2019-10-04 01:33:41 +02:00
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
/**
2019-10-04 19:29:21 +02:00
* Fetch relay data from a given server url
2019-10-04 01:33:41 +02:00
*
* @ param string $server_url address of the server
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
2020-05-17 15:59:05 +02:00
private static function discoverRelay ( string $server_url )
2019-10-04 01:33:41 +02:00
{
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 ;
}
2020-05-17 08:13:58 +02:00
// Sanitize incoming data, see https://github.com/friendica/friendica/issues/8565
$data [ 'subscribe' ] = ( bool ) $data [ 'subscribe' ] ? ? false ;
if ( ! $data [ 'subscribe' ] || empty ( $data [ 'scope' ]) || ! in_array ( strtolower ( $data [ 'scope' ]), [ 'all' , 'tags' ])) {
$data [ 'scope' ] = '' ;
$data [ 'subscribe' ] = false ;
$data [ 'tags' ] = [];
}
2019-10-04 01:33:41 +02:00
$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-04 19:29:21 +02:00
/**
* Fetch server data from '/statistics.json' on the given server
*
* @ param string $url URL of the given server
*
* @ return array server data
*/
private static function fetchStatistics ( string $url )
2019-10-03 15:02:48 +02:00
{
$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' ])) {
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $data [ 'network' ]);
2019-10-03 15:02:48 +02:00
2020-01-12 22:07:40 +01:00
if ( $serverdata [ 'platform' ] == 'diaspora' ) {
2019-10-03 15:02:48 +02:00
$serverdata [ 'network' ] = Protocol :: DIASPORA ;
2020-01-12 22:07:40 +01:00
} elseif ( $serverdata [ 'platform' ] == 'friendica' ) {
2019-10-03 15:02:48 +02:00
$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
/**
2019-10-04 19:29:21 +02:00
* Detect server type by using the nodeinfo data
2019-10-03 11:20:36 +02:00
*
2019-12-30 03:51:16 +01:00
* @ param string $url address of the server
* @ param CurlResult $curlResult
2019-10-03 11:20:36 +02:00
* @ return array Server data
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
2019-12-30 03:51:16 +01:00
private static function fetchNodeinfo ( string $url , CurlResult $curlResult )
2019-10-03 11:20:36 +02:00
{
$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 ;
}
/**
2019-10-04 19:29:21 +02:00
* Parses Nodeinfo 1
2019-10-03 11:20:36 +02:00
*
* @ param string $nodeinfo_url address of the nodeinfo path
* @ return array Server data
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
2019-10-04 19:29:21 +02:00
private static function parseNodeinfo1 ( string $nodeinfo_url )
2019-10-03 11:20:36 +02:00
{
$curlResult = Network :: curl ( $nodeinfo_url );
if ( ! $curlResult -> isSuccess ()) {
2019-10-05 06:50:29 +02:00
return [];
2019-10-03 11:20:36 +02:00
}
$nodeinfo = json_decode ( $curlResult -> getBody (), true );
if ( ! is_array ( $nodeinfo )) {
2019-10-05 06:50:29 +02:00
return [];
2019-10-03 11:20:36 +02:00
}
$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' ])) {
2020-01-12 22:07:40 +01:00
$server [ 'platform' ] = strtolower ( $nodeinfo [ 'software' ][ 'name' ]);
2019-10-03 11:20:36 +02:00
}
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
}
}
2019-10-05 06:50:29 +02:00
if ( empty ( $server )) {
return [];
2019-10-03 11:20:36 +02:00
}
return $server ;
}
/**
2019-10-04 19:29:21 +02:00
* Parses Nodeinfo 2
2019-10-03 11:20:36 +02:00
*
* @ param string $nodeinfo_url address of the nodeinfo path
* @ return array Server data
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
2019-10-04 19:29:21 +02:00
private static function parseNodeinfo2 ( string $nodeinfo_url )
2019-10-03 11:20:36 +02:00
{
$curlResult = Network :: curl ( $nodeinfo_url );
if ( ! $curlResult -> isSuccess ()) {
2019-10-05 06:50:29 +02:00
return [];
2019-10-03 11:20:36 +02:00
}
$nodeinfo = json_decode ( $curlResult -> getBody (), true );
if ( ! is_array ( $nodeinfo )) {
2019-10-05 06:50:29 +02:00
return [];
2019-10-03 11:20:36 +02:00
}
$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' ])) {
2020-01-12 22:07:40 +01:00
$server [ 'platform' ] = strtolower ( $nodeinfo [ 'software' ][ 'name' ]);
2019-10-03 11:20:36 +02:00
}
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 ;
}
2019-12-21 14:48:20 +01:00
if ( ! empty ( $protocols [ 'dfrn' ])) {
2019-10-03 11:20:36 +02:00
$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 )) {
2019-10-05 06:50:29 +02:00
return [];
2019-10-03 11:20:36 +02:00
}
return $server ;
}
2019-10-04 19:29:21 +02:00
/**
2019-10-05 06:22:16 +02:00
* Fetch server information from a 'siteinfo.json' file on the given server
2019-10-04 19:29:21 +02:00
*
* @ param string $url URL of the given server
* @ param array $serverdata array with server data
*
* @ return array server data
*/
private static function fetchSiteinfo ( string $url , array $serverdata )
2019-10-03 11:20:36 +02:00
{
$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' ])) {
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $data [ 'platform' ]);
2019-10-03 11:20:36 +02:00
$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-04 19:29:21 +02:00
/**
* Checks if the server contains a valid host meta file
*
* @ param string $url URL of the given server
*
2019-10-05 06:22:16 +02:00
* @ return boolean 'true' if the server seems to be vital
2019-10-04 19:29:21 +02:00
*/
private static function validHostMeta ( string $url )
2019-10-03 22:52:04 +02:00
{
2020-01-19 21:21:13 +01:00
$xrd_timeout = DI :: config () -> get ( 'system' , 'xrd_timeout' );
2019-10-03 22:52:04 +02:00
$curlResult = Network :: curl ( $url . '/.well-known/host-meta' , false , [ 'timeout' => $xrd_timeout ]);
if ( ! $curlResult -> isSuccess ()) {
return false ;
}
2020-04-27 16:35:50 +02:00
$xrd = XML :: parseString ( $curlResult -> getBody ());
2019-10-03 22:52:04 +02:00
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 ) {
2019-10-06 18:33:39 +02:00
// When there is more than a single "link" element, the array looks slightly different
if ( ! empty ( $link [ '@attributes' ])) {
$link = $link [ '@attributes' ];
}
if ( empty ( $link [ 'rel' ]) || empty ( $link [ 'template' ])) {
2019-10-03 22:52:04 +02:00
continue ;
}
2019-10-06 18:33:39 +02:00
if ( $link [ 'rel' ] == 'lrdd' ) {
2019-10-03 22:52:04 +02:00
// 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-04 19:29:21 +02:00
/**
* Detect the network of the given server via their known contacts
*
* @ param string $url URL of the given server
* @ param array $serverdata array with server data
*
* @ return array server data
*/
private static function detectNetworkViaContacts ( string $url , array $serverdata )
2019-10-04 08:33:16 +02:00
{
2019-10-06 18:33:39 +02:00
$contacts = [];
2019-10-04 08:33:16 +02:00
2019-10-06 01:30:47 +02:00
$gcontacts = DBA :: select ( 'gcontact' , [ 'url' , 'nurl' ], [ 'server_url' => [ $url , $serverdata [ 'nurl' ]]]);
2019-10-04 08:33:16 +02:00
while ( $gcontact = DBA :: fetch ( $gcontacts )) {
$contacts [ $gcontact [ 'nurl' ]] = $gcontact [ 'url' ];
}
DBA :: close ( $gcontacts );
2019-10-06 01:30:47 +02:00
$apcontacts = DBA :: select ( 'apcontact' , [ 'url' ], [ 'baseurl' => [ $url , $serverdata [ 'nurl' ]]]);
2020-04-27 16:35:50 +02:00
while ( $apcontact = DBA :: fetch ( $apcontacts )) {
2019-10-06 01:30:47 +02:00
$contacts [ Strings :: normaliseLink ( $apcontact [ 'url' ])] = $apcontact [ 'url' ];
2019-10-04 08:33:16 +02:00
}
DBA :: close ( $apcontacts );
2019-10-06 01:30:47 +02:00
$pcontacts = DBA :: select ( 'contact' , [ 'url' , 'nurl' ], [ 'uid' => 0 , 'baseurl' => [ $url , $serverdata [ 'nurl' ]]]);
2020-04-27 16:35:50 +02:00
while ( $pcontact = DBA :: fetch ( $pcontacts )) {
2019-10-04 08:33:16 +02:00
$contacts [ $pcontact [ 'nurl' ]] = $pcontact [ 'url' ];
}
DBA :: close ( $pcontacts );
if ( empty ( $contacts )) {
return $serverdata ;
}
foreach ( $contacts as $contact ) {
$probed = Probe :: uri ( $contact );
if ( in_array ( $probed [ 'network' ], Protocol :: FEDERATED )) {
$serverdata [ 'network' ] = $probed [ 'network' ];
break ;
}
}
$serverdata [ 'registered-users' ] = max ( $serverdata [ 'registered-users' ], count ( $contacts ));
return $serverdata ;
}
2019-10-04 19:29:21 +02:00
/**
2019-10-05 06:22:16 +02:00
* Checks if the given server does have a '/poco' endpoint .
* This is used for the 'PortableContact' functionality ,
* which is used by both Friendica and Hubzilla .
2019-10-04 19:29:21 +02:00
*
* @ param string $url URL of the given server
* @ param array $serverdata array with server data
*
* @ return array server data
*/
private static function checkPoCo ( string $url , array $serverdata )
2019-10-03 17:30:07 +02:00
{
2019-12-21 14:48:20 +01:00
$serverdata [ 'poco' ] = '' ;
2019-10-03 17:30:07 +02:00
$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 );
2019-12-21 14:48:20 +01:00
$serverdata [ 'directory-type' ] = self :: DT_POCO ;
2019-10-03 17:30:07 +02:00
$serverdata [ 'poco' ] = $url . '/poco' ;
2019-12-21 14:48:20 +01:00
}
return $serverdata ;
}
/**
* Checks if the given server does have a Mastodon style directory endpoint .
*
* @ param string $url URL of the given server
* @ param array $serverdata array with server data
*
* @ return array server data
*/
public static function checkMastodonDirectory ( string $url , array $serverdata )
{
2019-12-24 06:04:11 +01:00
$curlResult = Network :: curl ( $url . '/api/v1/directory?limit=1' );
2019-12-21 14:48:20 +01:00
if ( ! $curlResult -> isSuccess ()) {
return $serverdata ;
}
$data = json_decode ( $curlResult -> getBody (), true );
if ( empty ( $data )) {
return $serverdata ;
}
if ( count ( $data ) == 1 ) {
$serverdata [ 'directory-type' ] = self :: DT_MASTODON ;
2019-10-03 17:30:07 +02:00
}
return $serverdata ;
}
2019-10-04 19:29:21 +02:00
/**
* Detects the version number of a given server when it was a NextCloud installation
*
* @ param string $url URL of the given server
* @ param array $serverdata array with server data
*
* @ return array server data
*/
private static function detectNextcloud ( string $url , array $serverdata )
2019-10-03 11:20:36 +02:00
{
$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 ;
}
2019-10-04 19:29:21 +02:00
/**
* Detects data from a given server url if it was a mastodon alike system
*
* @ param string $url URL of the given server
* @ param array $serverdata array with server data
*
* @ return array server data
*/
private static function detectMastodonAlikes ( string $url , array $serverdata )
2019-10-03 11:20:36 +02:00
{
$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' ;
2019-10-16 14:35:14 +02:00
$serverdata [ 'version' ] = $data [ 'version' ] ? ? '' ;
2019-10-03 11:20:36 +02:00
$serverdata [ 'network' ] = Protocol :: ACTIVITYPUB ;
}
2019-10-03 15:02:48 +02:00
if ( ! empty ( $data [ 'title' ])) {
$serverdata [ 'site_name' ] = $data [ 'title' ];
}
2020-01-13 06:57:05 +01:00
if ( ! empty ( $data [ 'title' ]) && empty ( $serverdata [ 'platform' ]) && empty ( $serverdata [ 'network' ])) {
$serverdata [ 'platform' ] = 'mastodon' ;
$serverdata [ 'network' ] = Protocol :: ACTIVITYPUB ;
}
2019-10-03 15:02:48 +02:00
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 )) {
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $matches [ 1 ]);
2019-10-03 22:52:04 +02:00
$serverdata [ 'version' ] = $matches [ 2 ];
}
2020-01-13 06:57:05 +01:00
if ( ! empty ( $serverdata [ 'version' ]) && strstr ( strtolower ( $serverdata [ 'version' ]), 'pleroma' )) {
$serverdata [ 'platform' ] = 'pleroma' ;
$serverdata [ 'version' ] = trim ( str_ireplace ( 'pleroma' , '' , $serverdata [ 'version' ]));
}
if ( ! empty ( $serverdata [ 'platform' ]) && strstr ( $serverdata [ 'platform' ], 'pleroma' )) {
$serverdata [ 'version' ] = trim ( str_ireplace ( 'pleroma' , '' , $serverdata [ 'platform' ]));
2019-10-03 11:20:36 +02:00
$serverdata [ 'platform' ] = 'pleroma' ;
}
return $serverdata ;
}
2019-10-04 19:29:21 +02:00
/**
* Detects data from typical Hubzilla endpoints
*
* @ param string $url URL of the given server
* @ param array $serverdata array with server data
*
* @ return array server data
*/
private static function detectHubzilla ( string $url , array $serverdata )
2019-10-03 15:02:48 +02:00
{
$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' ])) {
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $data [ 'site' ][ 'platform' ][ 'PLATFORM_NAME' ]);
2019-10-03 15:02:48 +02:00
$serverdata [ 'version' ] = $data [ 'site' ][ 'platform' ][ 'STD_VERSION' ];
$serverdata [ 'network' ] = Protocol :: ZOT ;
}
2019-10-03 16:48:46 +02:00
if ( ! empty ( $data [ 'site' ][ 'hubzilla' ])) {
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $data [ 'site' ][ 'hubzilla' ][ 'PLATFORM_NAME' ]);
2019-10-03 15:02:48 +02:00
$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' ])) {
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $data [ 'site' ][ 'redmatrix' ][ 'PLATFORM_NAME' ]);
2019-10-03 16:48:46 +02:00
} elseif ( ! empty ( $data [ 'site' ][ 'redmatrix' ][ 'RED_PLATFORM' ])) {
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $data [ 'site' ][ 'redmatrix' ][ 'RED_PLATFORM' ]);
2019-10-03 15:02:48 +02:00
}
$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 ;
}
2019-10-04 19:29:21 +02:00
/**
* Converts input value to a boolean value
*
* @ param string | integer $val
*
* @ return boolean
*/
2019-10-03 15:02:48 +02:00
private static function toBoolean ( $val )
{
if (( $val == 'true' ) || ( $val == 1 )) {
return true ;
} elseif (( $val == 'false' ) || ( $val == 0 )) {
return false ;
}
return $val ;
}
2019-10-04 19:29:21 +02:00
/**
* Detect if the URL belongs to a GNU Social server
*
* @ param string $url URL of the given server
* @ param array $serverdata array with server data
*
* @ return array server data
*/
private static function detectGNUSocial ( string $url , array $serverdata )
2019-10-03 11:20:36 +02:00
{
2019-10-06 18:33:39 +02:00
// Test for GNU Social
$curlResult = Network :: curl ( $url . '/api/gnusocial/version.json' );
2019-10-03 11:20:36 +02:00
if ( $curlResult -> isSuccess () && ( $curlResult -> getBody () != '{"error":"not implemented"}' ) &&
( $curlResult -> getBody () != '' ) && ( strlen ( $curlResult -> getBody ()) < 30 )) {
2019-10-06 18:33:39 +02:00
$serverdata [ 'platform' ] = 'gnusocial' ;
2019-10-03 11:20:36 +02:00
// Remove junk that some GNU Social servers return
2019-10-06 18:33:39 +02:00
$serverdata [ 'version' ] = str_replace ( chr ( 239 ) . chr ( 187 ) . chr ( 191 ), '' , $curlResult -> getBody ());
2020-01-12 13:50:00 +01:00
$serverdata [ 'version' ] = str_replace ([ " \r " , " \n " , " \t " ], '' , $serverdata [ 'version' ]);
2019-10-03 11:20:36 +02:00
$serverdata [ 'version' ] = trim ( $serverdata [ 'version' ], '"' );
$serverdata [ 'network' ] = Protocol :: OSTATUS ;
2019-10-06 18:33:39 +02:00
return $serverdata ;
2019-10-03 11:20:36 +02:00
}
2019-10-06 18:33:39 +02:00
// Test for Statusnet
$curlResult = Network :: curl ( $url . '/api/statusnet/version.json' );
2019-10-03 11:20:36 +02:00
if ( $curlResult -> isSuccess () && ( $curlResult -> getBody () != '{"error":"not implemented"}' ) &&
( $curlResult -> getBody () != '' ) && ( strlen ( $curlResult -> getBody ()) < 30 )) {
2020-01-12 13:50:00 +01:00
2019-10-03 11:20:36 +02:00
// Remove junk that some GNU Social servers return
2019-10-06 18:33:39 +02:00
$serverdata [ 'version' ] = str_replace ( chr ( 239 ) . chr ( 187 ) . chr ( 191 ), '' , $curlResult -> getBody ());
2020-01-12 13:50:00 +01:00
$serverdata [ 'version' ] = str_replace ([ " \r " , " \n " , " \t " ], '' , $serverdata [ 'version' ]);
2019-10-03 11:20:36 +02:00
$serverdata [ 'version' ] = trim ( $serverdata [ 'version' ], '"' );
2020-01-12 13:50:00 +01:00
if ( ! empty ( $serverdata [ 'version' ]) && strtolower ( substr ( $serverdata [ 'version' ], 0 , 7 )) == 'pleroma' ) {
$serverdata [ 'platform' ] = 'pleroma' ;
$serverdata [ 'version' ] = trim ( str_ireplace ( 'pleroma' , '' , $serverdata [ 'version' ]));
$serverdata [ 'network' ] = Protocol :: ACTIVITYPUB ;
} else {
$serverdata [ 'platform' ] = 'statusnet' ;
$serverdata [ 'network' ] = Protocol :: OSTATUS ;
}
2019-10-03 11:20:36 +02:00
}
return $serverdata ;
}
2019-10-04 19:29:21 +02:00
/**
* Detect if the URL belongs to a Friendica server
*
* @ param string $url URL of the given server
* @ param array $serverdata array with server data
*
* @ return array server data
*/
private static function detectFriendica ( string $url , array $serverdata )
2019-10-03 11:20:36 +02:00
{
$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' ]);
}
2019-10-16 14:35:14 +02:00
$register_policy = ( $data [ 'register_policy' ] ? ? '' ) ? : 'REGISTER_CLOSED' ;
2019-10-03 11:20:36 +02:00
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 ;
}
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $data [ 'platform' ] ? ? '' );
2019-10-03 11:20:36 +02:00
return $serverdata ;
}
2019-10-04 19:29:21 +02:00
/**
* Analyses the landing page of a given server for hints about type and system of that server
*
* @ param object $curlResult result of curl execution
* @ param array $serverdata array with server data
2019-10-06 18:33:39 +02:00
* @ param string $url Server URL
2019-10-04 19:29:21 +02:00
*
* @ return array server data
*/
2019-10-06 18:33:39 +02:00
private static function analyseRootBody ( $curlResult , array $serverdata , string $url )
2019-10-03 11:20:36 +02:00
{
$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 ) {
2019-11-19 11:02:35 +01:00
$value = trim ( $attribute -> value );
if ( empty ( $value )) {
2019-10-03 11:20:36 +02:00
continue ;
}
2019-11-19 11:02:35 +01:00
$attr [ $attribute -> name ] = $value ;
2019-10-03 11:20:36 +02:00
}
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' ) {
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $attr [ 'content' ]);
2019-10-03 11:20:36 +02:00
if ( in_array ( $attr [ 'content' ], [ 'Misskey' , 'Write.as' ])) {
$serverdata [ 'network' ] = Protocol :: ACTIVITYPUB ;
}
}
2020-01-13 06:57:05 +01:00
if (( $attr [ 'name' ] == 'generator' ) && ( empty ( $serverdata [ 'platform' ]) || ( substr ( strtolower ( $attr [ 'content' ]), 0 , 9 ) == 'wordpress' ))) {
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $attr [ 'content' ]);
2019-10-03 11:20:36 +02:00
$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' ])) {
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $version_part [ 0 ]);
2019-10-03 11:20:36 +02:00
$serverdata [ 'version' ] = $version_part [ 1 ];
2019-10-06 18:33:39 +02:00
// We still do need a reliable test if some AP plugin is activated
if ( DBA :: exists ( 'apcontact' , [ 'baseurl' => $url ])) {
$serverdata [ 'network' ] = Protocol :: ACTIVITYPUB ;
} else {
$serverdata [ 'network' ] = Protocol :: FEED ;
}
2019-10-03 11:20:36 +02:00
}
if ( in_array ( $version_part [ 0 ], [ 'Friendika' , 'Friendica' ])) {
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $version_part [ 0 ]);
2019-10-03 11:20:36 +02:00
$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 ) {
2019-11-19 11:02:35 +01:00
$value = trim ( $attribute -> value );
if ( empty ( $value )) {
2019-10-03 11:20:36 +02:00
continue ;
}
2019-11-19 11:02:35 +01:00
$attr [ $attribute -> name ] = $value ;
2019-10-03 11:20:36 +02:00
}
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' ) {
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $attr [ 'content' ]);
2019-10-03 12:44:29 +02:00
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' ) {
2020-01-12 22:07:40 +01:00
$serverdata [ 'platform' ] = strtolower ( $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 ;
}
2019-10-04 19:29:21 +02:00
/**
* Analyses the header data of a given server for hints about type and system of that server
*
* @ param object $curlResult result of curl execution
* @ param array $serverdata array with server data
*
* @ return array server data
*/
private static function analyseRootHeader ( $curlResult , array $serverdata )
2019-10-03 11:20:36 +02:00
{
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 ;
}
2019-12-21 07:39:22 +01:00
2020-01-12 13:50:00 +01:00
/**
* Test if the body contains valid content
*
* @ param string $body
* @ return boolean
*/
private static function invalidBody ( string $body )
{
// Currently we only test for a HTML element.
// Possibly we enhance this in the future.
return ! strpos ( $body , '>' );
}
2019-12-21 07:39:22 +01:00
/**
* Update the user directory of a given gserver record
2019-12-21 14:48:20 +01:00
*
* @ param array $gserver gserver record
2019-12-21 07:39:22 +01:00
*/
public static function updateDirectory ( array $gserver )
{
/// @todo Add Mastodon API directory
2019-12-21 14:48:20 +01:00
2019-12-21 07:39:22 +01:00
if ( ! empty ( $gserver [ 'poco' ])) {
PortableContact :: discoverSingleServer ( $gserver [ 'id' ]);
}
}
2020-01-01 22:29:36 +01:00
/**
* Update GServer entries
*/
public static function discover ()
{
// Update the server list
self :: discoverFederation ();
$no_of_queries = 5 ;
2020-01-19 21:21:13 +01:00
$requery_days = intval ( DI :: config () -> get ( 'system' , 'poco_requery_days' ));
2020-01-01 22:29:36 +01:00
if ( $requery_days == 0 ) {
$requery_days = 7 ;
}
$last_update = date ( 'c' , time () - ( 60 * 60 * 24 * $requery_days ));
$gservers = DBA :: p ( " SELECT `id`, `url`, `nurl`, `network`, `poco`
FROM `gserver`
WHERE `last_contact` >= `last_failure`
AND `poco` != ''
AND `last_poco_query` < ?
ORDER BY RAND () " , $last_update
);
while ( $gserver = DBA :: fetch ( $gservers )) {
if ( ! GServer :: check ( $gserver [ 'url' ], $gserver [ 'network' ])) {
// The server is not reachable? Okay, then we will try it later
$fields = [ 'last_poco_query' => DateTimeFormat :: utcNow ()];
DBA :: update ( 'gserver' , $fields , [ 'nurl' => $gserver [ 'nurl' ]]);
continue ;
}
Logger :: info ( 'Update directory' , [ 'server' => $gserver [ 'url' ], 'id' => $gserver [ 'id' ]]);
Worker :: add ( PRIORITY_LOW , 'UpdateServerDirectory' , $gserver );
if ( -- $no_of_queries == 0 ) {
break ;
}
}
DBA :: close ( $gservers );
}
/**
* Discover federated servers
*/
private static function discoverFederation ()
{
2020-01-19 21:21:13 +01:00
$last = DI :: config () -> get ( 'poco' , 'last_federation_discovery' );
2020-01-01 22:29:36 +01:00
if ( $last ) {
$next = $last + ( 24 * 60 * 60 );
if ( $next > time ()) {
return ;
}
}
// Discover federated servers
$curlResult = Network :: fetchUrl ( " http://the-federation.info/pods.json " );
if ( ! empty ( $curlResult )) {
$servers = json_decode ( $curlResult , true );
if ( ! empty ( $servers [ 'pods' ])) {
foreach ( $servers [ 'pods' ] as $server ) {
Worker :: add ( PRIORITY_LOW , 'UpdateGServer' , 'https://' . $server [ 'host' ]);
}
}
}
// Disvover Mastodon servers
2020-01-19 21:21:13 +01:00
$accesstoken = DI :: config () -> get ( 'system' , 'instances_social_key' );
2020-01-01 22:29:36 +01:00
if ( ! empty ( $accesstoken )) {
$api = 'https://instances.social/api/1.0/instances/list?count=0' ;
$header = [ 'Authorization: Bearer ' . $accesstoken ];
$curlResult = Network :: curl ( $api , false , [ 'headers' => $header ]);
if ( $curlResult -> isSuccess ()) {
$servers = json_decode ( $curlResult -> getBody (), true );
foreach ( $servers [ 'instances' ] as $server ) {
$url = ( is_null ( $server [ 'https_score' ]) ? 'http' : 'https' ) . '://' . $server [ 'name' ];
Worker :: add ( PRIORITY_LOW , 'UpdateGServer' , $url );
}
}
}
2020-01-19 21:21:53 +01:00
DI :: config () -> set ( 'poco' , 'last_federation_discovery' , time ());
2020-01-01 22:29:36 +01:00
}
2019-10-03 11:20:36 +02:00
}