2021-10-04 17:00:58 +01:00
< ? php
2021-10-10 09:26:18 +01:00
declare ( strict_types = 1 );
2021-10-04 17:00:58 +01:00
// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social 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.
//
// GNU social 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 GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}}
namespace Component\FreeNetwork ;
2021-12-23 13:17:37 +00:00
use App\Core\DB\DB ;
2021-10-18 13:22:02 +01:00
use App\Core\Event ;
2021-12-02 04:25:58 +00:00
use App\Core\GSFile ;
use App\Core\HTTPClient ;
2021-10-18 13:22:02 +01:00
use function App\Core\I18n\_m ;
use App\Core\Log ;
2021-10-04 17:00:58 +01:00
use App\Core\Modules\Component ;
2021-10-18 13:22:02 +01:00
use App\Core\Router\RouteLoader ;
2021-10-27 04:15:07 +01:00
use App\Core\Router\Router ;
2021-12-23 13:17:37 +00:00
use App\Entity\Activity ;
2021-10-18 13:22:02 +01:00
use App\Entity\Actor ;
use App\Entity\LocalUser ;
use App\Entity\Note ;
use App\Util\Common ;
use App\Util\Exception\ClientException ;
2021-12-23 13:17:37 +00:00
use App\Util\Exception\NicknameEmptyException ;
use App\Util\Exception\NicknameException ;
use App\Util\Exception\NicknameInvalidException ;
use App\Util\Exception\NicknameNotAllowedException ;
use App\Util\Exception\NicknameTakenException ;
use App\Util\Exception\NicknameTooLongException ;
2021-10-18 13:22:02 +01:00
use App\Util\Exception\NoSuchActorException ;
use App\Util\Exception\ServerException ;
2022-02-23 17:39:11 +00:00
use App\Util\Formatting ;
2021-10-18 13:22:02 +01:00
use App\Util\Nickname ;
2021-12-23 13:17:37 +00:00
use Component\FreeNetwork\Controller\Feeds ;
2021-10-18 13:22:02 +01:00
use Component\FreeNetwork\Controller\HostMeta ;
use Component\FreeNetwork\Controller\OwnerXrd ;
use Component\FreeNetwork\Controller\Webfinger ;
use Component\FreeNetwork\Util\Discovery ;
use Component\FreeNetwork\Util\WebfingerResource ;
use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceActor ;
2021-10-27 04:15:07 +01:00
use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceNote ;
2022-02-23 17:39:11 +00:00
use Doctrine\Common\Collections\ExpressionBuilder ;
2021-10-18 13:22:02 +01:00
use Exception ;
2021-12-23 13:17:37 +00:00
use const PREG_SET_ORDER ;
2021-10-18 13:22:02 +01:00
use Symfony\Component\HttpFoundation\JsonResponse ;
use Symfony\Component\HttpFoundation\Response ;
2021-12-23 13:17:37 +00:00
use XML_XRD ;
2021-10-18 13:22:02 +01:00
use XML_XRD_Element_Link ;
2021-10-04 17:00:58 +01:00
2021-10-18 13:22:02 +01:00
/**
* Implements WebFinger ( RFC7033 ) for GNU social , as well as Link - based Resource Descriptor Discovery based on RFC6415 ,
* Web Host Metadata ( '.well-known/host-meta' ) resource .
*
* @ package GNUsocial
*
* @ author Mikael Nordfeldth < mmn @ hethane . se >
* @ author Diogo Peralta Cordeiro < mail @ diogo . site >
*/
2021-10-04 17:00:58 +01:00
class FreeNetwork extends Component
{
2021-10-27 04:15:07 +01:00
public const PLUGIN_VERSION = '0.1.0' ;
2021-10-18 13:22:02 +01:00
2021-10-27 04:15:07 +01:00
public const OAUTH_ACCESS_TOKEN_REL = 'http://apinamespace.org/oauth/access_token' ;
public const OAUTH_REQUEST_TOKEN_REL = 'http://apinamespace.org/oauth/request_token' ;
public const OAUTH_AUTHORIZE_REL = 'http://apinamespace.org/oauth/authorize' ;
2022-02-23 17:39:11 +00:00
private static array $protocols = [];
2022-02-21 04:53:12 +00:00
2022-02-23 17:39:11 +00:00
public function onInitializeComponent () : bool
2022-02-21 04:53:12 +00:00
{
Event :: handle ( 'AddFreeNetworkProtocol' , [ & self :: $protocols ]);
2022-02-23 17:39:11 +00:00
return Event :: next ;
2022-02-21 04:53:12 +00:00
}
2021-10-18 13:22:02 +01:00
public function onAddRoute ( RouteLoader $m ) : bool
{
2021-12-23 13:17:37 +00:00
// Feeds
$m -> connect ( 'feed_network' , '/feed/network' , [ Feeds :: class , 'network' ]);
$m -> connect ( 'feed_clique' , '/feed/clique' , [ Feeds :: class , 'clique' ]);
$m -> connect ( 'feed_federated' , '/feed/federated' , [ Feeds :: class , 'federated' ]);
2021-10-18 13:22:02 +01:00
$m -> connect ( 'freenetwork_hostmeta' , '.well-known/host-meta' , [ HostMeta :: class , 'handle' ]);
2021-10-27 04:15:07 +01:00
$m -> connect (
'freenetwork_hostmeta_format' ,
'.well-known/host-meta.:format' ,
2021-10-18 13:22:02 +01:00
[ HostMeta :: class , 'handle' ],
2021-10-27 04:15:07 +01:00
[ 'format' => '(xml|json)' ],
);
2021-10-18 13:22:02 +01:00
// the resource GET parameter can be anywhere, so don't mention it here
$m -> connect ( 'freenetwork_webfinger' , '.well-known/webfinger' , [ Webfinger :: class , 'handle' ]);
2021-10-27 04:15:07 +01:00
$m -> connect (
'freenetwork_webfinger_format' ,
'.well-known/webfinger.:format' ,
2021-10-18 13:22:02 +01:00
[ Webfinger :: class , 'handle' ],
2021-10-27 04:15:07 +01:00
[ 'format' => '(xml|json)' ],
);
2021-10-18 13:22:02 +01:00
$m -> connect ( 'freenetwork_ownerxrd' , 'main/ownerxrd' , [ OwnerXrd :: class , 'handle' ]);
return Event :: next ;
}
2021-12-23 13:17:37 +00:00
public function onCreateDefaultFeeds ( int $actor_id , LocalUser $user , int & $ordering )
{
DB :: persist ( \App\Entity\Feed :: create ([ 'actor_id' => $actor_id , 'url' => Router :: url ( $route = 'feed_network' ), 'route' => $route , 'title' => _m ( 'Meteorites' ), 'ordering' => $ordering ++ ]));
DB :: persist ( \App\Entity\Feed :: create ([ 'actor_id' => $actor_id , 'url' => Router :: url ( $route = 'feed_clique' ), 'route' => $route , 'title' => _m ( 'Planetary System' ), 'ordering' => $ordering ++ ]));
DB :: persist ( \App\Entity\Feed :: create ([ 'actor_id' => $actor_id , 'url' => Router :: url ( $route = 'feed_federated' ), 'route' => $route , 'title' => _m ( 'Galaxy' ), 'ordering' => $ordering ++ ]));
return Event :: next ;
}
2021-10-27 04:15:07 +01:00
public function onStartGetProfileAcctUri ( Actor $profile , & $acct ) : bool
2021-10-18 13:22:02 +01:00
{
$wfr = new WebFingerResourceActor ( $profile );
try {
$acct = $wfr -> reconstructAcct ();
2021-10-27 04:15:07 +01:00
} catch ( Exception ) {
return Event :: next ;
2021-10-18 13:22:02 +01:00
}
2021-10-27 04:15:07 +01:00
return Event :: stop ;
2021-10-18 13:22:02 +01:00
}
2021-12-04 19:52:14 +00:00
/**
* Last attempts getting a WebFingerResource object
*
2021-12-23 13:17:37 +00:00
* @ param string $resource String that contains the requested URI
* @ param null | WebfingerResource $target WebFingerResource extended object goes here
* @ param array $args Array which may contains arguments such as 'rel' filtering values
*
2021-12-04 19:52:14 +00:00
* @ throws NicknameEmptyException
* @ throws NicknameException
* @ throws NicknameInvalidException
* @ throws NicknameNotAllowedException
* @ throws NicknameTakenException
* @ throws NicknameTooLongException
2021-12-23 13:17:37 +00:00
* @ throws NoSuchActorException
* @ throws ServerException
2021-12-04 19:52:14 +00:00
*/
public function onEndGetWebFingerResource ( string $resource , ? WebfingerResource & $target = null , array $args = []) : bool
2021-10-18 13:22:02 +01:00
{
// * Either we didn't find the profile, then we want to make
// the $profile variable null for clarity.
// * Or we did find it but for a possibly malicious remote
// user who might've set their profile URL to a Note URL
// which would've caused a sort of DoS unless we continue
// our search here by discarding the remote profile.
$profile = null ;
if ( Discovery :: isAcct ( $resource )) {
2021-10-27 04:15:07 +01:00
$parts = explode ( '@' , mb_substr ( urldecode ( $resource ), 5 )); // 5 is strlen of 'acct:'
2021-12-23 13:17:37 +00:00
if ( \count ( $parts ) === 2 ) {
2021-10-27 04:15:07 +01:00
[ $nick , $domain ] = $parts ;
2022-02-11 00:17:20 +00:00
if ( $domain !== Common :: config ( 'site' , 'server' )) {
2021-10-18 13:22:02 +01:00
throw new ServerException ( _m ( 'Remote profiles not supported via WebFinger yet.' ));
}
2021-10-27 04:15:07 +01:00
$nick = Nickname :: normalize ( nickname : $nick , check_already_used : false , check_is_allowed : false );
2021-12-16 11:14:34 +00:00
$freenetwork_actor = LocalUser :: getByPK ([ 'nickname' => $nick ]);
2021-10-18 13:22:02 +01:00
if ( ! ( $freenetwork_actor instanceof LocalUser )) {
throw new NoSuchActorException ( $nick );
}
$profile = $freenetwork_actor -> getActor ();
}
2021-10-27 04:15:07 +01:00
} else {
2021-10-18 13:22:02 +01:00
try {
2021-11-01 12:16:46 +00:00
if ( Common :: isValidHttpUrl ( $resource )) {
2021-10-27 04:15:07 +01:00
// This means $resource is a valid url
$resource_parts = parse_url ( $resource );
// TODO: Use URLMatcher
2022-02-11 00:17:20 +00:00
if ( $resource_parts [ 'host' ] === Common :: config ( 'site' , 'server' )) {
2021-10-27 04:15:07 +01:00
$str = $resource_parts [ 'path' ];
// actor_view_nickname
$renick = '/\/@(' . Nickname :: DISPLAY_FMT . ')\/?/m' ;
// actor_view_id
$reuri = '/\/actor\/(\d+)\/?/m' ;
2021-11-27 04:12:44 +00:00
if ( preg_match_all ( $renick , $str , $matches , PREG_SET_ORDER , 0 ) === 1 ) {
2021-12-16 11:14:34 +00:00
$profile = LocalUser :: getByPK ([ 'nickname' => $matches [ 0 ][ 1 ]]) -> getActor ();
2021-11-27 04:12:44 +00:00
} elseif ( preg_match_all ( $reuri , $str , $matches , PREG_SET_ORDER , 0 ) === 1 ) {
2021-10-27 04:15:07 +01:00
$profile = Actor :: getById (( int ) $matches [ 0 ][ 1 ]);
}
2021-10-18 13:22:02 +01:00
}
}
2021-10-27 04:15:07 +01:00
} catch ( NoSuchActorException $e ) {
2021-10-18 13:22:02 +01:00
// not a User, maybe a Note? we'll try that further down...
// try {
// Log::debug(__METHOD__ . ': Finding User_group URI for WebFinger lookup on resource==' . $resource);
// $group = new User_group();
// $group->whereAddIn('uri', array_keys($alt_urls), $group->columnType('uri'));
// $group->limit(1);
// if ($group->find(true)) {
// $profile = $group->getProfile();
// }
// unset($group);
// } catch (Exception $e) {
// Log::error(get_class($e) . ': ' . $e->getMessage());
// throw $e;
// }
2021-10-27 04:15:07 +01:00
}
2021-10-18 13:22:02 +01:00
}
if ( $profile instanceof Actor ) {
Log :: debug ( __METHOD__ . ': Found Profile with ID==' . $profile -> getID () . ' for resource==' . $resource );
$target = new WebfingerResourceActor ( $profile );
2021-10-27 04:15:07 +01:00
return Event :: stop ; // We got our target, stop handler execution
2021-10-18 13:22:02 +01:00
}
2022-01-02 03:14:27 +00:00
if ( ! \is_null ( $note = DB :: findOneBy ( Note :: class , [ 'url' => $resource ], return_null : true ))) {
$target = new WebfingerResourceNote ( $note );
2021-10-27 04:15:07 +01:00
return Event :: stop ; // We got our target, stop handler execution
2021-10-18 13:22:02 +01:00
}
2021-10-27 04:15:07 +01:00
return Event :: next ;
2021-10-18 13:22:02 +01:00
}
2021-10-27 04:15:07 +01:00
public function onStartHostMetaLinks ( array & $links ) : bool
2021-10-18 13:22:02 +01:00
{
foreach ( Discovery :: supportedMimeTypes () as $type ) {
2021-10-27 04:15:07 +01:00
$links [] = new XML_XRD_Element_Link (
Discovery :: LRDD_REL ,
2021-10-18 13:22:02 +01:00
Router :: url ( id : 'freenetwork_webfinger' , args : [], type : Router :: ABSOLUTE_URL ) . '?resource={uri}' ,
$type ,
2021-10-27 04:15:07 +01:00
isTemplate : true ,
);
2021-10-18 13:22:02 +01:00
}
// TODO OAuth connections
//$links[] = new XML_XRD_Element_link(self::OAUTH_ACCESS_TOKEN_REL, common_local_url('ApiOAuthAccessToken'));
//$links[] = new XML_XRD_Element_link(self::OAUTH_REQUEST_TOKEN_REL, common_local_url('ApiOAuthRequestToken'));
//$links[] = new XML_XRD_Element_link(self::OAUTH_AUTHORIZE_REL, common_local_url('ApiOAuthAuthorize'));
2021-10-27 04:15:07 +01:00
return Event :: next ;
2021-10-18 13:22:02 +01:00
}
/**
* Add a link header for LRDD Discovery
*/
2021-10-27 04:15:07 +01:00
public function onStartShowHTML ( $action ) : bool
2021-10-18 13:22:02 +01:00
{
if ( $action instanceof ShowstreamAction ) {
$resource = $action -> getTarget () -> getUri ();
$url = common_local_url ( 'webfinger' ) . '?resource=' . urlencode ( $resource );
foreach ([ Discovery :: JRD_MIMETYPE , Discovery :: XRD_MIMETYPE ] as $type ) {
header ( 'Link: <' . $url . '>; rel="' . Discovery :: LRDD_REL . '"; type="' . $type . '"' , false );
}
}
2021-10-27 04:15:07 +01:00
return Event :: next ;
2021-10-18 13:22:02 +01:00
}
2021-10-27 04:15:07 +01:00
public function onStartDiscoveryMethodRegistration ( Discovery $disco ) : bool
2021-10-18 13:22:02 +01:00
{
2021-10-27 04:15:07 +01:00
$disco -> registerMethod ( '\Component\FreeNetwork\Util\LrddMethod\LrddMethodWebfinger' );
return Event :: next ;
2021-10-18 13:22:02 +01:00
}
2021-10-27 04:15:07 +01:00
public function onEndDiscoveryMethodRegistration ( Discovery $disco ) : bool
2021-10-18 13:22:02 +01:00
{
2021-10-27 04:15:07 +01:00
$disco -> registerMethod ( '\Component\FreeNetwork\Util\LrddMethod\LrddMethodHostMeta' );
$disco -> registerMethod ( '\Component\FreeNetwork\Util\LrddMethod\LrddMethodLinkHeader' );
$disco -> registerMethod ( '\Component\FreeNetwork\Util\LrddMethod\LrddMethodLinkHtml' );
return Event :: next ;
2021-10-18 13:22:02 +01:00
}
/**
* @ throws ClientException
* @ throws ServerException
*/
2022-01-02 03:14:27 +00:00
public function onControllerResponseInFormat ( string $route , array $accept_header , array $vars , ? Response & $response = null ) : bool
2021-10-18 13:22:02 +01:00
{
2021-12-23 13:17:37 +00:00
if ( ! \in_array ( $route , [ 'freenetwork_hostmeta' , 'freenetwork_hostmeta_format' , 'freenetwork_webfinger' , 'freenetwork_webfinger_format' , 'freenetwork_ownerxrd' ])) {
2021-10-18 13:22:02 +01:00
return Event :: next ;
}
$mimeType = array_intersect ( array_values ( Discovery :: supportedMimeTypes ()), $accept_header );
/*
* " A WebFinger resource MUST return a JRD as the representation
* for the resource if the client requests no other supported
* format explicitly via the HTTP " Accept " header . [ ... ]
* The WebFinger resource MUST silently ignore any requested
* representations that it does not understand and support . "
* -- RFC 7033 ( WebFinger )
* http :// tools . ietf . org / html / rfc7033
*/
2021-12-23 13:17:37 +00:00
$mimeType = \count ( $mimeType ) !== 0 ? array_pop ( $mimeType ) : $vars [ 'default_mimetype' ];
2021-10-18 13:22:02 +01:00
$headers = [];
if ( Common :: config ( 'discovery' , 'cors' )) {
$headers [ 'Access-Control-Allow-Origin' ] = '*' ;
}
$headers [ 'Content-Type' ] = $mimeType ;
$response = match ( $mimeType ) {
Discovery :: XRD_MIMETYPE => new Response ( content : $vars [ 'xrd' ] -> to ( 'xml' ), headers : $headers ),
Discovery :: JRD_MIMETYPE , Discovery :: JRD_MIMETYPE_OLD => new JsonResponse ( data : $vars [ 'xrd' ] -> to ( 'json' ), headers : $headers , json : true ),
};
2022-01-02 03:14:27 +00:00
$response -> headers -> set ( 'cache-control' , 'no-store, no-cache, must-revalidate' );
2021-10-18 13:22:02 +01:00
return Event :: stop ;
}
2021-12-02 03:34:31 +00:00
/**
* Webfinger matches : @ user @ example . com or even @ user -- one . george_orwell @ 1984. biz
*
2021-12-23 13:17:37 +00:00
* @ param string $text The text from which to extract webfinger IDs
2021-12-02 03:34:31 +00:00
* @ param string $preMention Character ( s ) that signals a mention ( '@' , '!' ... )
2021-12-23 13:17:37 +00:00
*
* @ return array the matching IDs ( without $preMention ) and each respective position in the given string
2021-12-02 03:34:31 +00:00
*/
public static function extractWebfingerIds ( string $text , string $preMention = '@' ) : array
{
$wmatches = [];
2021-12-23 13:17:37 +00:00
$result = preg_match_all (
2021-12-02 03:34:31 +00:00
'/' . Nickname :: BEFORE_MENTIONS . preg_quote ( $preMention , '/' ) . '(' . Nickname :: WEBFINGER_FMT . ')/' ,
$text ,
$wmatches ,
2021-12-23 13:17:37 +00:00
\PREG_OFFSET_CAPTURE ,
2021-12-02 03:34:31 +00:00
);
if ( $result === false ) {
Log :: error ( __METHOD__ . ': Error parsing webfinger IDs from text (preg_last_error==' . preg_last_error () . ').' );
return [];
2021-12-23 13:17:37 +00:00
} elseif (( $n_matches = \count ( $wmatches )) != 0 ) {
2021-12-02 03:34:31 +00:00
Log :: debug (( sprintf ( 'Found %d matches for WebFinger IDs: %s' , $n_matches , print_r ( $wmatches , true ))));
}
return $wmatches [ 1 ];
}
/**
* Profile URL matches : @ param string $text The text from which to extract URL mentions
2021-12-23 13:17:37 +00:00
*
2021-12-02 03:34:31 +00:00
* @ param string $preMention Character ( s ) that signals a mention ( '@' , '!' ... )
*
2021-12-23 13:17:37 +00:00
* @ return array the matching URLs ( without @ or acct : ) and each respective position in the given string
* @ example . com / mublog / user
2021-12-02 03:34:31 +00:00
*/
public static function extractUrlMentions ( string $text , string $preMention = '@' ) : array
{
$wmatches = [];
// In the regexp below we need to match / _before_ URL_REGEX_VALID_PATH_CHARS because it otherwise gets merged
// with the TLD before (but / is in URL_REGEX_VALID_PATH_CHARS anyway, it's just its positioning that is important)
$result = preg_match_all (
2021-12-02 04:25:58 +00:00
'/' . Nickname :: BEFORE_MENTIONS . preg_quote ( $preMention , '/' ) . '(' . URL_REGEX_DOMAIN_NAME . '(?:\/[' . URL_REGEX_VALID_PATH_CHARS . ']*)*)/' ,
2021-12-02 03:34:31 +00:00
$text ,
$wmatches ,
2021-12-23 13:17:37 +00:00
\PREG_OFFSET_CAPTURE ,
2021-12-02 03:34:31 +00:00
);
if ( $result === false ) {
Log :: error ( __METHOD__ . ': Error parsing profile URL mentions from text (preg_last_error==' . preg_last_error () . ').' );
return [];
2021-12-23 13:17:37 +00:00
} elseif ( \count ( $wmatches )) {
Log :: debug (( sprintf ( 'Found %d matches for profile URL mentions: %s' , \count ( $wmatches ), print_r ( $wmatches , true ))));
2021-12-02 03:34:31 +00:00
}
return $wmatches [ 1 ];
}
/**
* Find any explicit remote mentions . Accepted forms :
* Webfinger : @ user @ example . com
* Profile link : @ param Actor $sender
2021-12-23 13:17:37 +00:00
*
2021-12-02 03:34:31 +00:00
* @ param string $text input markup text
* @ param $mentions
2021-12-23 13:17:37 +00:00
*
2021-12-02 03:34:31 +00:00
* @ return bool hook return value
* @ example . com / mublog / user
*/
public function onEndFindMentions ( Actor $sender , string $text , array & $mentions ) : bool
{
$matches = [];
foreach ( self :: extractWebfingerIds ( $text , $preMention = '@' ) as $wmatch ) {
[ $target , $pos ] = $wmatch ;
2021-12-23 13:17:37 +00:00
Log :: info ( " Checking webfinger person ' { $target } ' " );
2021-12-02 03:34:31 +00:00
$actor = null ;
$resource_parts = explode ( $preMention , $target );
2022-02-11 00:17:20 +00:00
if ( $resource_parts [ 1 ] === Common :: config ( 'site' , 'server' )) {
2021-12-16 11:14:34 +00:00
$actor = LocalUser :: getByPK ([ 'nickname' => $resource_parts [ 0 ]]) -> getActor ();
2021-12-02 03:34:31 +00:00
} else {
Event :: handle ( 'FreeNetworkFindMentions' , [ $target , & $actor ]);
2021-12-23 13:17:37 +00:00
if ( \is_null ( $actor )) {
2021-12-02 03:34:31 +00:00
continue ;
}
}
2021-12-23 13:17:37 +00:00
\assert ( $actor instanceof Actor );
2021-12-02 03:34:31 +00:00
2021-12-12 06:32:17 +00:00
$displayName = ! empty ( $actor -> getFullname ()) ? $actor -> getFullname () : $actor -> getNickname () ? ? $target ; // TODO: we could do getBestName() or getFullname() here
2021-12-02 03:34:31 +00:00
$matches [ $pos ] = [
'mentioned' => [ $actor ],
2021-12-23 13:17:37 +00:00
'type' => 'mention' ,
'text' => $displayName ,
'position' => $pos ,
'length' => mb_strlen ( $target ),
'url' => $actor -> getUri (),
2021-12-02 03:34:31 +00:00
];
}
foreach ( self :: extractUrlMentions ( $text ) as $wmatch ) {
[ $target , $pos ] = $wmatch ;
2021-12-23 13:17:37 +00:00
$url = " https:// { $target } " ;
2021-12-02 04:25:58 +00:00
if ( Common :: isValidHttpUrl ( $url )) {
// This means $resource is a valid url
$resource_parts = parse_url ( $url );
// TODO: Use URLMatcher
2022-02-11 00:17:20 +00:00
if ( $resource_parts [ 'host' ] === Common :: config ( 'site' , 'server' )) {
2021-12-02 04:25:58 +00:00
$str = $resource_parts [ 'path' ];
// actor_view_nickname
$renick = '/\/@(' . Nickname :: DISPLAY_FMT . ')\/?/m' ;
// actor_view_id
$reuri = '/\/actor\/(\d+)\/?/m' ;
if ( preg_match_all ( $renick , $str , $matches , PREG_SET_ORDER , 0 ) === 1 ) {
2021-12-16 11:14:34 +00:00
$actor = LocalUser :: getByPK ([ 'nickname' => $matches [ 0 ][ 1 ]]) -> getActor ();
2021-12-02 04:25:58 +00:00
} elseif ( preg_match_all ( $reuri , $str , $matches , PREG_SET_ORDER , 0 ) === 1 ) {
$actor = Actor :: getById (( int ) $matches [ 0 ][ 1 ]);
2021-12-02 03:34:31 +00:00
} else {
2021-12-02 04:25:58 +00:00
Log :: error ( 'Unexpected behaviour onEndFindMentions at FreeNetwork' );
throw new ServerException ( 'Unexpected behaviour onEndFindMentions at FreeNetwork' );
2021-12-02 03:34:31 +00:00
}
} else {
2021-12-23 13:17:37 +00:00
Log :: info ( " Checking actor address ' { $url } ' " );
2021-12-02 04:25:58 +00:00
$link = new XML_XRD_Element_Link (
Discovery :: LRDD_REL ,
2021-12-23 13:17:37 +00:00
'https://' . parse_url ( $url , \PHP_URL_HOST ) . '/.well-known/webfinger?resource={uri}' ,
2021-12-02 04:25:58 +00:00
Discovery :: JRD_MIMETYPE ,
2021-12-23 13:17:37 +00:00
true , // isTemplate
2021-12-02 04:25:58 +00:00
);
2021-12-23 13:17:37 +00:00
$xrd_uri = Discovery :: applyTemplate ( $link -> template , $url );
2021-12-02 04:25:58 +00:00
$response = HTTPClient :: get ( $xrd_uri , [ 'headers' => [ 'Accept' => $link -> type ]]);
if ( $response -> getStatusCode () !== 200 ) {
continue ;
}
$xrd = new XML_XRD ();
switch ( GSFile :: mimetypeBare ( $response -> getHeaders ()[ 'content-type' ][ 0 ])) {
case Discovery :: JRD_MIMETYPE_OLD :
case Discovery :: JRD_MIMETYPE :
$type = 'json' ;
break ;
case Discovery :: XRD_MIMETYPE :
$type = 'xml' ;
break ;
default :
// fall back to letting XML_XRD auto-detect
Log :: debug ( 'No recognized content-type header for resource descriptor body on ' . $xrd_uri );
$type = null ;
}
$xrd -> loadString ( $response -> getContent (), $type );
$actor = null ;
Event :: handle ( 'FreeNetworkFoundXrd' , [ $xrd , & $actor ]);
2021-12-23 13:17:37 +00:00
if ( \is_null ( $actor )) {
2021-12-02 04:25:58 +00:00
continue ;
}
2021-12-02 03:34:31 +00:00
}
2021-12-23 13:17:37 +00:00
$displayName = $actor -> getFullname () ? ? $actor -> getNickname () ? ? $target ; // TODO: we could do getBestName() or getFullname() here
2021-12-02 04:25:58 +00:00
$matches [ $pos ] = [
'mentioned' => [ $actor ],
2021-12-23 13:17:37 +00:00
'type' => 'mention' ,
'text' => $displayName ,
'position' => $pos ,
'length' => mb_strlen ( $target ),
'url' => $actor -> getUri (),
2021-12-02 04:25:58 +00:00
];
2021-12-02 03:34:31 +00:00
}
}
foreach ( $mentions as $i => $other ) {
// If we share a common prefix with a local user, override it!
$pos = $other [ 'position' ];
if ( isset ( $matches [ $pos ])) {
$mentions [ $i ] = $matches [ $pos ];
unset ( $matches [ $pos ]);
}
}
foreach ( $matches as $mention ) {
$mentions [] = $mention ;
}
return Event :: next ;
}
2021-11-29 23:58:27 +00:00
public static function notify ( Actor $sender , Activity $activity , array $targets , ? string $reason = null ) : bool
2021-11-27 04:12:44 +00:00
{
2022-02-21 04:53:12 +00:00
foreach ( self :: $protocols as $protocol ) {
2022-03-05 14:23:08 +00:00
$protocol :: freeNetworkDistribute ( $sender , $activity , $targets , $reason );
2021-11-29 23:58:27 +00:00
}
2021-11-27 04:12:44 +00:00
return false ;
}
2022-02-21 04:52:30 +00:00
public static function mentionTagToName ( string $nickname , string $uri ) : string
2021-12-27 04:13:09 +00:00
{
return '@' . $nickname . '@' . parse_url ( $uri , \PHP_URL_HOST );
}
2022-02-21 04:52:30 +00:00
public static function groupTagToName ( string $nickname , string $uri ) : string
{
return '!' . $nickname . '@' . parse_url ( $uri , \PHP_URL_HOST );
}
2022-02-21 04:53:12 +00:00
/**
* Add fediverse : query expression
* // TODO: adding WebFinger would probably be nice
*/
public function onCollectionQueryCreateExpression ( ExpressionBuilder $eb , string $term , ? string $locale , ? Actor $actor , & $note_expr , & $actor_expr ) : bool
{
if ( Formatting :: startsWith ( $term , [ 'fediverse:' ])) {
foreach ( self :: $protocols as $protocol ) {
// 10 is strlen of `fediverse:`
if ( $protocol :: freeNetworkGrabRemote ( mb_substr ( $term , 10 ))) {
break ;
}
}
}
return Event :: next ;
}
2021-10-18 13:22:02 +01:00
public function onPluginVersion ( array & $versions ) : bool
{
$versions [] = [
'name' => 'WebFinger' ,
'version' => self :: PLUGIN_VERSION ,
'author' => 'Mikael Nordfeldth' ,
'homepage' => GNUSOCIAL_ENGINE_URL ,
// TRANS: Plugin description.
'rawdescription' => _m ( 'WebFinger and LRDD support' ),
];
return true ;
}
2021-10-10 09:26:18 +01:00
}