2018-09-10 21:07:25 +00:00
< ? php
/**
2023-01-01 09:36:24 -05:00
* @ copyright Copyright ( C ) 2010 - 2023 , the Friendica project
2020-02-09 16:18:46 +01:00
*
* @ 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 />.
*
2018-09-10 21:07:25 +00:00
*/
2020-02-09 16:18:46 +01:00
2018-09-10 21:07:25 +00:00
namespace Friendica\Protocol ;
2021-11-03 19:06:07 +00:00
use Friendica\Core\Logger ;
2018-09-10 21:07:25 +00:00
use Friendica\Core\Protocol ;
2023-07-21 07:06:55 +00:00
use Friendica\Core\System ;
2023-12-22 17:16:10 +00:00
use Friendica\DI ;
2018-09-26 17:24:29 +00:00
use Friendica\Model\APContact ;
2023-07-21 07:06:55 +00:00
use Friendica\Model\Contact ;
2019-07-04 19:31:42 +00:00
use Friendica\Model\User ;
2018-11-03 21:37:08 +00:00
use Friendica\Util\HTTPSignature ;
2020-03-04 22:07:05 +01:00
use Friendica\Util\JsonLD ;
2018-09-10 21:07:25 +00:00
/**
2020-01-19 06:05:23 +00:00
* ActivityPub Protocol class
*
2018-09-10 21:07:25 +00:00
* The ActivityPub Protocol is a message exchange protocol defined by the W3C .
* https :// www . w3 . org / TR / activitypub /
* https :// www . w3 . org / TR / activitystreams - core /
* https :// www . w3 . org / TR / activitystreams - vocabulary /
*
* https :// blog . joinmastodon . org / 2018 / 06 / how - to - implement - a - basic - activitypub - server /
* https :// blog . joinmastodon . org / 2018 / 07 / how - to - make - friends - and - verify - requests /
2018-09-12 06:01:28 +00:00
*
* Digest : https :// tools . ietf . org / html / rfc5843
* https :// tools . ietf . org / html / draft - cavage - http - signatures - 10 #ref-15
2018-09-15 18:54:45 +00:00
*
2018-09-23 17:29:31 +00:00
* Mastodon implementation of supported activities :
* https :// github . com / tootsuite / mastodon / blob / master / app / lib / activitypub / activity . rb #L26
*
2018-10-04 05:29:22 +00:00
* Funkwhale :
* http :// docs - funkwhale - funkwhale - 549 - music - federation - documentation . preview . funkwhale . audio / federation / index . html
*
2018-09-15 10:14:56 +00:00
* To - do :
2018-09-23 17:29:31 +00:00
* - Polling the outboxes for missing content ?
2018-10-24 19:19:51 +00:00
*
* Missing parts from DFRN :
2023-05-30 09:15:17 -04:00
* - Public Group
* - Private Group
2018-10-24 19:19:51 +00:00
* - Relocation
2018-09-10 21:07:25 +00:00
*/
class ActivityPub
{
2018-09-28 15:25:20 +00:00
const PUBLIC_COLLECTION = 'https://www.w3.org/ns/activitystreams#Public' ;
2023-07-23 14:27:08 +00:00
const CONTEXT = [
'https://www.w3.org/ns/activitystreams' , 'https://w3id.org/security/v1' ,
[
'vcard' => 'http://www.w3.org/2006/vcard/ns#' ,
'dfrn' => 'http://purl.org/macgirvin/dfrn/1.0/' ,
'diaspora' => 'https://diasporafoundation.org/ns/' ,
'litepub' => 'http://litepub.social/ns#' ,
'toot' => 'http://joinmastodon.org/ns#' ,
'featured' => [
" @id " => " toot:featured " ,
" @type " => " @id " ,
],
'schema' => 'http://schema.org#' ,
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers' ,
'sensitive' => 'as:sensitive' , 'Hashtag' => 'as:Hashtag' ,
'quoteUrl' => 'as:quoteUrl' ,
'conversation' => 'ostatus:conversation' ,
'directMessage' => 'litepub:directMessage' ,
'discoverable' => 'toot:discoverable' ,
'PropertyValue' => 'schema:PropertyValue' ,
'value' => 'schema:value' ,
]
];
2020-12-15 22:56:46 +00:00
const ACCOUNT_TYPES = [ 'Person' , 'Organization' , 'Service' , 'Group' , 'Application' , 'Tombstone' ];
2018-09-26 21:38:37 +00:00
/**
2018-10-06 04:18:40 +00:00
* Checks if the web request is done for the AP protocol
2018-09-26 21:38:37 +00:00
*
2019-01-06 16:06:53 -05:00
* @ return bool is it AP ?
2018-09-26 21:38:37 +00:00
*/
2022-06-23 10:30:44 +02:00
public static function isRequest () : bool
2018-09-16 20:12:48 +00:00
{
2023-07-08 21:01:48 -04:00
header ( 'Vary: Accept' , false );
2021-11-03 19:06:07 +00:00
$isrequest = stristr ( $_SERVER [ 'HTTP_ACCEPT' ] ? ? '' , 'application/activity+json' ) ||
2021-01-29 11:15:47 +00:00
stristr ( $_SERVER [ 'HTTP_ACCEPT' ] ? ? '' , 'application/json' ) ||
2019-10-16 08:35:14 -04:00
stristr ( $_SERVER [ 'HTTP_ACCEPT' ] ? ? '' , 'application/ld+json' );
2021-11-03 19:06:07 +00:00
if ( $isrequest ) {
Logger :: debug ( 'Is AP request' , [ 'accept' => $_SERVER [ 'HTTP_ACCEPT' ], 'agent' => $_SERVER [ 'HTTP_USER_AGENT' ] ? ? '' ]);
}
return $isrequest ;
2018-09-16 20:12:48 +00:00
}
2022-06-16 14:59:29 +02:00
private static function getAccountType ( array $apcontact ) : int
2019-07-04 19:31:42 +00:00
{
$accounttype = - 1 ;
2023-07-23 14:27:08 +00:00
switch ( $apcontact [ 'type' ]) {
2019-07-04 19:31:42 +00:00
case 'Person' :
$accounttype = User :: ACCOUNT_TYPE_PERSON ;
break ;
case 'Organization' :
$accounttype = User :: ACCOUNT_TYPE_ORGANISATION ;
break ;
case 'Service' :
$accounttype = User :: ACCOUNT_TYPE_NEWS ;
break ;
case 'Group' :
$accounttype = User :: ACCOUNT_TYPE_COMMUNITY ;
break ;
case 'Application' :
$accounttype = User :: ACCOUNT_TYPE_RELAY ;
break ;
2020-12-15 22:56:46 +00:00
case 'Tombstone' :
$accounttype = User :: ACCOUNT_TYPE_DELETED ;
break ;
2020-12-16 15:43:12 +00:00
}
2019-07-04 19:31:42 +00:00
return $accounttype ;
}
2018-09-26 21:38:37 +00:00
/**
2018-10-03 09:15:38 +00:00
* Fetches a profile from the given url into an array that is compatible to Probe :: uri
2018-09-26 21:38:37 +00:00
*
2019-04-08 20:41:18 +00:00
* @ param string $url profile url
* @ param boolean $update Update the profile
2018-10-03 09:15:38 +00:00
* @ return array
2019-01-06 16:06:53 -05:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-09-26 21:38:37 +00:00
*/
2022-06-16 14:59:29 +02:00
public static function probeProfile ( string $url , bool $update = true ) : array
2018-09-14 21:10:49 +00:00
{
2019-04-08 20:41:18 +00:00
$apcontact = APContact :: getByURL ( $url , $update );
2018-10-03 09:15:38 +00:00
if ( empty ( $apcontact )) {
2020-06-14 13:37:28 +00:00
return [];
2018-09-14 21:10:49 +00:00
}
2018-10-03 09:15:38 +00:00
$profile = [ 'network' => Protocol :: ACTIVITYPUB ];
$profile [ 'nick' ] = $apcontact [ 'nick' ];
$profile [ 'name' ] = $apcontact [ 'name' ];
$profile [ 'guid' ] = $apcontact [ 'uuid' ];
$profile [ 'url' ] = $apcontact [ 'url' ];
$profile [ 'addr' ] = $apcontact [ 'addr' ];
$profile [ 'alias' ] = $apcontact [ 'alias' ];
2019-07-08 12:00:11 +00:00
$profile [ 'following' ] = $apcontact [ 'following' ];
$profile [ 'followers' ] = $apcontact [ 'followers' ];
$profile [ 'inbox' ] = $apcontact [ 'inbox' ];
$profile [ 'outbox' ] = $apcontact [ 'outbox' ];
$profile [ 'sharedinbox' ] = $apcontact [ 'sharedinbox' ];
2018-10-03 09:15:38 +00:00
$profile [ 'photo' ] = $apcontact [ 'photo' ];
2021-06-17 11:23:32 +00:00
$profile [ 'header' ] = $apcontact [ 'header' ];
2019-07-04 19:31:42 +00:00
$profile [ 'account-type' ] = self :: getAccountType ( $apcontact );
$profile [ 'community' ] = ( $profile [ 'account-type' ] == User :: ACCOUNT_TYPE_COMMUNITY );
2018-10-03 09:15:38 +00:00
// $profile['keywords']
// $profile['location']
$profile [ 'about' ] = $apcontact [ 'about' ];
2021-08-10 23:49:09 +00:00
$profile [ 'xmpp' ] = $apcontact [ 'xmpp' ];
$profile [ 'matrix' ] = $apcontact [ 'matrix' ];
2018-10-03 09:15:38 +00:00
$profile [ 'batch' ] = $apcontact [ 'sharedinbox' ];
$profile [ 'notify' ] = $apcontact [ 'inbox' ];
$profile [ 'poll' ] = $apcontact [ 'outbox' ];
$profile [ 'pubkey' ] = $apcontact [ 'pubkey' ];
2020-06-04 21:55:14 +00:00
$profile [ 'subscribe' ] = $apcontact [ 'subscribe' ];
2020-09-02 03:02:50 +00:00
$profile [ 'manually-approve' ] = $apcontact [ 'manually-approve' ];
2018-10-03 09:15:38 +00:00
$profile [ 'baseurl' ] = $apcontact [ 'baseurl' ];
2020-05-22 10:10:24 +00:00
$profile [ 'gsid' ] = $apcontact [ 'gsid' ];
2018-09-14 21:10:49 +00:00
2021-06-30 05:40:11 +00:00
if ( ! is_null ( $apcontact [ 'discoverable' ])) {
$profile [ 'hide' ] = ! $apcontact [ 'discoverable' ];
}
2018-10-03 09:15:38 +00:00
// Remove all "null" fields
foreach ( $profile as $field => $content ) {
if ( is_null ( $content )) {
unset ( $profile [ $field ]);
2018-09-14 21:10:49 +00:00
}
}
2018-10-03 09:15:38 +00:00
return $profile ;
2018-09-16 09:06:09 +00:00
}
2018-09-26 21:38:37 +00:00
/**
2018-10-06 04:18:40 +00:00
* Fetches activities from the outbox of a given profile and processes it
2018-09-26 21:38:37 +00:00
*
2019-01-06 16:06:53 -05:00
* @ param string $url
2018-10-03 09:15:38 +00:00
* @ param integer $uid User ID
2022-06-23 10:30:44 +02:00
* @ return void
2019-01-06 16:06:53 -05:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-09-30 20:26:30 +00:00
*/
2022-06-16 14:59:29 +02:00
public static function fetchOutbox ( string $url , int $uid )
2018-09-30 20:26:30 +00:00
{
2023-11-09 06:43:03 +00:00
$data = HTTPSignature :: fetch ( $url , $uid );
2018-10-03 09:15:38 +00:00
if ( empty ( $data )) {
2018-09-30 20:26:30 +00:00
return ;
}
2018-10-03 09:15:38 +00:00
if ( ! empty ( $data [ 'orderedItems' ])) {
$items = $data [ 'orderedItems' ];
} elseif ( ! empty ( $data [ 'first' ][ 'orderedItems' ])) {
$items = $data [ 'first' ][ 'orderedItems' ];
} elseif ( ! empty ( $data [ 'first' ])) {
self :: fetchOutbox ( $data [ 'first' ], $uid );
2018-09-30 20:26:30 +00:00
return ;
} else {
2018-10-03 09:15:38 +00:00
$items = [];
2018-09-15 18:54:45 +00:00
}
2018-10-03 09:15:38 +00:00
foreach ( $items as $activity ) {
2018-10-07 17:17:06 +00:00
$ldactivity = JsonLD :: compact ( $activity );
2022-07-21 05:16:14 +00:00
ActivityPub\Receiver :: processActivity ( $ldactivity , '' , $uid , true );
2018-09-15 18:54:45 +00:00
}
}
2019-07-27 11:09:12 +00:00
2020-03-05 08:12:29 +00:00
/**
* Fetch items from AP endpoints
*
2023-09-29 04:50:36 +00:00
* @ param string $url Address of the endpoint
* @ param integer $uid Optional user id
* @ param integer $start_timestamp Internally used parameter to stop fetching after some time
2020-03-05 08:12:29 +00:00
* @ return array Endpoint items
*/
2023-09-29 04:50:36 +00:00
public static function fetchItems ( string $url , int $uid = 0 , int $start_timestamp = 0 ) : array
2020-03-05 08:12:29 +00:00
{
2023-09-29 04:50:36 +00:00
$start_timestamp = $start_timestamp ? : time ();
2023-09-28 08:04:52 +00:00
2023-09-29 04:50:36 +00:00
if (( time () - $start_timestamp ) > 60 ) {
2023-09-28 08:04:52 +00:00
Logger :: info ( 'Fetch time limit reached' , [ 'url' => $url , 'uid' => $uid ]);
return [];
}
2023-11-09 06:43:03 +00:00
$data = HTTPSignature :: fetch ( $url , $uid );
2020-03-05 08:12:29 +00:00
if ( empty ( $data )) {
return [];
}
if ( ! empty ( $data [ 'orderedItems' ])) {
$items = $data [ 'orderedItems' ];
} elseif ( ! empty ( $data [ 'first' ][ 'orderedItems' ])) {
$items = $data [ 'first' ][ 'orderedItems' ];
2020-03-22 13:05:35 +00:00
} elseif ( ! empty ( $data [ 'first' ]) && is_string ( $data [ 'first' ]) && ( $data [ 'first' ] != $url )) {
2023-09-29 04:50:36 +00:00
return self :: fetchItems ( $data [ 'first' ], $uid , $start_timestamp );
2020-03-05 08:12:29 +00:00
} else {
2020-03-28 10:34:23 +00:00
return [];
2020-03-05 08:12:29 +00:00
}
2020-03-05 22:03:24 +00:00
if ( ! empty ( $data [ 'next' ]) && is_string ( $data [ 'next' ])) {
2023-09-29 04:50:36 +00:00
$items = array_merge ( $items , self :: fetchItems ( $data [ 'next' ], $uid , $start_timestamp ));
2020-03-05 08:12:29 +00:00
}
return $items ;
}
2019-07-27 11:09:12 +00:00
/**
* Checks if the given contact url does support ActivityPub
*
* @ param string $url profile url
2019-07-27 21:45:36 +00:00
* @ param boolean $update true = always update , false = never update , null = update when not found or outdated
2019-07-27 11:09:12 +00:00
* @ return boolean
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
*/
2022-06-23 10:30:44 +02:00
public static function isSupportedByContactUrl ( string $url , $update = null ) : bool
2019-07-27 11:09:12 +00:00
{
return ! empty ( APContact :: getByURL ( $url , $update ));
}
2023-07-21 07:06:55 +00:00
public static function isAcceptedRequester ( int $uid = 0 ) : bool
{
$called_by = System :: callstack ( 1 );
$signer = HTTPSignature :: getSigner ( '' , $_SERVER );
if ( ! $signer ) {
2023-07-23 13:59:01 +00:00
Logger :: debug ( 'No signer or invalid signature' , [ 'uid' => $uid , 'agent' => $_SERVER [ 'HTTP_USER_AGENT' ] ? ? '' , 'called_by' => $called_by ]);
2023-07-21 07:06:55 +00:00
return false ;
}
$apcontact = APContact :: getByURL ( $signer );
if ( empty ( $apcontact )) {
2023-07-23 13:59:01 +00:00
Logger :: info ( 'APContact not found' , [ 'uid' => $uid , 'handle' => $signer , 'called_by' => $called_by ]);
2023-07-21 07:06:55 +00:00
return false ;
}
if ( empty ( $apcontact [ 'gsid' ] || empty ( $apcontact [ 'baseurl' ]))) {
Logger :: debug ( 'No server found' , [ 'uid' => $uid , 'signer' => $signer , 'called_by' => $called_by ]);
return false ;
}
$contact = Contact :: getByURL ( $signer , false , [ 'id' , 'baseurl' , 'gsid' ]);
if ( ! empty ( $contact ) && Contact\User :: isBlocked ( $contact [ 'id' ], $uid )) {
Logger :: info ( 'Requesting contact is blocked' , [ 'uid' => $uid , 'id' => $contact [ 'id' ], 'signer' => $signer , 'baseurl' => $contact [ 'baseurl' ], 'called_by' => $called_by ]);
return false ;
}
2023-12-22 17:16:10 +00:00
$limited = DI :: config () -> get ( 'system' , 'limited_servers' );
if ( ! empty ( $limited )) {
$servers = explode ( ',' , str_replace ( ' ' , '' , $limited ));
$host = parse_url ( $contact [ 'baseurl' ], PHP_URL_HOST );
if ( ! empty ( $host ) && in_array ( $host , $servers )) {
return false ;
}
}
2023-07-21 07:06:55 +00:00
// @todo Look for user blocked domains
2023-07-22 16:00:09 +00:00
Logger :: debug ( 'Server is an accepted requester' , [ 'uid' => $uid , 'id' => $apcontact [ 'gsid' ], 'url' => $apcontact [ 'baseurl' ], 'signer' => $signer , 'called_by' => $called_by ]);
2023-07-21 07:06:55 +00:00
return true ;
}
2018-09-10 21:07:25 +00:00
}