2022-12-19 23:08:11 +00:00
|
|
|
<?php
|
|
|
|
namespace Activitypub;
|
|
|
|
|
2023-10-22 22:20:53 +00:00
|
|
|
use WP_Error;
|
|
|
|
use Activitypub\Collection\Users;
|
|
|
|
|
2022-12-19 23:08:11 +00:00
|
|
|
/**
|
|
|
|
* ActivityPub WebFinger Class
|
|
|
|
*
|
|
|
|
* @author Matthias Pfefferle
|
|
|
|
*
|
|
|
|
* @see https://webfinger.net/
|
|
|
|
*/
|
|
|
|
class Webfinger {
|
|
|
|
/**
|
|
|
|
* Returns a users WebFinger "resource"
|
|
|
|
*
|
2024-02-08 12:31:25 +00:00
|
|
|
* @param int $user_id The WordPress user id
|
2022-12-19 23:08:11 +00:00
|
|
|
*
|
|
|
|
* @return string The user-resource
|
|
|
|
*/
|
|
|
|
public static function get_user_resource( $user_id ) {
|
2023-10-22 22:20:53 +00:00
|
|
|
$user = Users::get_by_id( $user_id );
|
|
|
|
if ( ! $user || is_wp_error( $user ) ) {
|
|
|
|
return '';
|
|
|
|
}
|
2022-12-19 23:08:11 +00:00
|
|
|
|
2024-02-08 12:31:25 +00:00
|
|
|
return $user->get_webfinger();
|
2022-12-19 23:08:11 +00:00
|
|
|
}
|
|
|
|
|
2023-10-22 22:20:53 +00:00
|
|
|
/**
|
|
|
|
* Resolve a WebFinger resource
|
|
|
|
*
|
2024-02-08 12:31:25 +00:00
|
|
|
* @param string $uri The WebFinger Resource
|
2023-10-22 22:20:53 +00:00
|
|
|
*
|
|
|
|
* @return string|WP_Error The URL or WP_Error
|
|
|
|
*/
|
2024-02-08 12:31:25 +00:00
|
|
|
public static function resolve( $uri ) {
|
|
|
|
$data = self::get_data( $uri );
|
2023-12-08 23:23:11 +00:00
|
|
|
|
2024-02-08 12:31:25 +00:00
|
|
|
if ( \is_wp_error( $data ) ) {
|
|
|
|
return $data;
|
2022-12-19 23:08:11 +00:00
|
|
|
}
|
2023-12-08 23:23:11 +00:00
|
|
|
|
2024-02-08 12:31:25 +00:00
|
|
|
foreach ( $data['links'] as $link ) {
|
|
|
|
if (
|
|
|
|
'self' === $link['rel'] &&
|
|
|
|
'application/activity+json' === $link['type']
|
|
|
|
) {
|
|
|
|
return $link['href'];
|
|
|
|
}
|
2023-03-17 22:33:51 +00:00
|
|
|
}
|
|
|
|
|
2024-02-08 12:31:25 +00:00
|
|
|
return new WP_Error( 'webfinger_url_no_activitypub', null, $data );
|
|
|
|
}
|
2022-12-19 23:08:11 +00:00
|
|
|
|
2024-02-08 12:31:25 +00:00
|
|
|
/**
|
|
|
|
* Transform a URI to an acct <identifier>@<host>
|
|
|
|
*
|
|
|
|
* @param string $uri The URI (acct:, mailto:, http:, https:)
|
|
|
|
*
|
|
|
|
* @return string|WP_Error Error or acct URI
|
|
|
|
*/
|
|
|
|
public static function uri_to_acct( $uri ) {
|
|
|
|
$data = self::get_data( $uri );
|
2022-12-19 23:08:11 +00:00
|
|
|
|
2024-02-08 12:31:25 +00:00
|
|
|
if ( is_wp_error( $data ) ) {
|
|
|
|
return $data;
|
2022-12-19 23:08:11 +00:00
|
|
|
}
|
|
|
|
|
2024-02-08 12:31:25 +00:00
|
|
|
// check if subject is an acct URI
|
|
|
|
if (
|
|
|
|
isset( $data['subject'] ) &&
|
|
|
|
\str_starts_with( $data['subject'], 'acct:' )
|
|
|
|
) {
|
|
|
|
return $data['subject'];
|
2022-12-19 23:08:11 +00:00
|
|
|
}
|
|
|
|
|
2024-02-08 12:31:25 +00:00
|
|
|
// search for an acct URI in the aliases
|
|
|
|
if ( isset( $data['aliases'] ) ) {
|
|
|
|
foreach ( $data['aliases'] as $alias ) {
|
|
|
|
if ( \str_starts_with( $alias, 'acct:' ) ) {
|
|
|
|
return $alias;
|
|
|
|
}
|
2022-12-19 23:08:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-08 12:31:25 +00:00
|
|
|
return new WP_Error(
|
|
|
|
'webfinger_url_no_acct',
|
|
|
|
__( 'No acct URI found.', 'activitypub' ),
|
|
|
|
$data
|
|
|
|
);
|
2022-12-19 23:08:11 +00:00
|
|
|
}
|
2023-10-22 22:20:53 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert a URI string to an identifier and its host.
|
|
|
|
* Automatically adds acct: if it's missing.
|
|
|
|
*
|
|
|
|
* @param string $url The URI (acct:, mailto:, http:, https:)
|
|
|
|
*
|
|
|
|
* @return WP_Error|array Error reaction or array with
|
|
|
|
* identifier and host as values
|
|
|
|
*/
|
|
|
|
public static function get_identifier_and_host( $url ) {
|
|
|
|
// remove leading @
|
|
|
|
$url = ltrim( $url, '@' );
|
|
|
|
|
|
|
|
if ( ! preg_match( '/^([a-zA-Z+]+):/', $url, $match ) ) {
|
|
|
|
$identifier = 'acct:' . $url;
|
|
|
|
$scheme = 'acct';
|
|
|
|
} else {
|
|
|
|
$identifier = $url;
|
|
|
|
$scheme = $match[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
$host = null;
|
|
|
|
|
|
|
|
switch ( $scheme ) {
|
|
|
|
case 'acct':
|
|
|
|
case 'mailto':
|
|
|
|
case 'xmpp':
|
|
|
|
if ( strpos( $identifier, '@' ) !== false ) {
|
|
|
|
$host = substr( $identifier, strpos( $identifier, '@' ) + 1 );
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
$host = wp_parse_url( $identifier, PHP_URL_HOST );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( empty( $host ) ) {
|
2024-02-08 12:31:25 +00:00
|
|
|
return new WP_Error( 'webfinger_invalid_identifier', __( 'Invalid Identifier', 'activitypub' ) );
|
2023-10-22 22:20:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return array( $identifier, $host );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the WebFinger data for a given URI
|
|
|
|
*
|
2024-02-08 12:31:25 +00:00
|
|
|
* @param string $uri The Identifier: <identifier>@<host> or URI
|
2023-10-22 22:20:53 +00:00
|
|
|
*
|
|
|
|
* @return WP_Error|array Error reaction or array with
|
|
|
|
* identifier and host as values
|
|
|
|
*/
|
2024-02-08 12:31:25 +00:00
|
|
|
public static function get_data( $uri ) {
|
|
|
|
$identifier_and_host = self::get_identifier_and_host( $uri );
|
|
|
|
|
|
|
|
if ( is_wp_error( $identifier_and_host ) ) {
|
|
|
|
return $identifier_and_host;
|
|
|
|
}
|
|
|
|
|
|
|
|
$transient_key = self::generate_cache_key( $uri );
|
|
|
|
|
|
|
|
list( $identifier, $host ) = $identifier_and_host;
|
|
|
|
|
|
|
|
$data = \get_transient( $transient_key );
|
|
|
|
if ( $data ) {
|
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
2023-10-22 22:20:53 +00:00
|
|
|
$webfinger_url = 'https://' . $host . '/.well-known/webfinger?resource=' . rawurlencode( $identifier );
|
|
|
|
|
|
|
|
$response = wp_safe_remote_get(
|
|
|
|
$webfinger_url,
|
|
|
|
array(
|
|
|
|
'headers' => array( 'Accept' => 'application/jrd+json' ),
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
if ( is_wp_error( $response ) ) {
|
2024-02-08 12:31:25 +00:00
|
|
|
return new WP_Error(
|
|
|
|
'webfinger_url_not_accessible',
|
|
|
|
__( 'The WebFinger Resource is not accessible.', 'activitypub' ),
|
|
|
|
$webfinger_url
|
|
|
|
);
|
2023-10-22 22:20:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
$body = wp_remote_retrieve_body( $response );
|
2024-02-08 12:31:25 +00:00
|
|
|
$data = json_decode( $body, true );
|
|
|
|
|
|
|
|
\set_transient( $transient_key, $data, WEEK_IN_SECONDS );
|
2023-10-22 22:20:53 +00:00
|
|
|
|
2024-02-08 12:31:25 +00:00
|
|
|
return $data;
|
2023-10-22 22:20:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-02-08 12:31:25 +00:00
|
|
|
* Get the Remote-Follow endpoint for a given URI
|
2023-10-22 22:20:53 +00:00
|
|
|
*
|
2024-02-08 12:31:25 +00:00
|
|
|
* @return string|WP_Error Error or the Remote-Follow endpoint URI.
|
2023-10-22 22:20:53 +00:00
|
|
|
*/
|
|
|
|
public static function get_remote_follow_endpoint( $uri ) {
|
2024-02-08 12:31:25 +00:00
|
|
|
$data = self::get_data( $uri );
|
2023-10-22 22:20:53 +00:00
|
|
|
|
|
|
|
if ( is_wp_error( $data ) ) {
|
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( empty( $data['links'] ) ) {
|
2024-02-08 12:31:25 +00:00
|
|
|
return new WP_Error(
|
|
|
|
'webfinger_missing_links',
|
|
|
|
__( 'No valid Link elements found.', 'activitypub' ),
|
|
|
|
$data
|
|
|
|
);
|
2023-10-22 22:20:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
foreach ( $data['links'] as $link ) {
|
|
|
|
if ( 'http://ostatus.org/schema/1.0/subscribe' === $link['rel'] ) {
|
|
|
|
return $link['template'];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-08 12:31:25 +00:00
|
|
|
return new WP_Error(
|
|
|
|
'webfinger_missing_remote_follow_endpoint',
|
|
|
|
__( 'No valid Remote-Follow endpoint found.', 'activitypub' ),
|
|
|
|
$data
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate a cache key for a given URI
|
|
|
|
*
|
|
|
|
* @param string $uri A WebFinger Resource URI
|
|
|
|
*
|
|
|
|
* @return string The cache key
|
|
|
|
*/
|
|
|
|
public static function generate_cache_key( $uri ) {
|
|
|
|
$uri = ltrim( $uri, '@' );
|
|
|
|
|
|
|
|
if ( filter_var( $uri, FILTER_VALIDATE_EMAIL ) ) {
|
|
|
|
$uri = 'acct:' . $uri;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 'webfinger_' . md5( $uri );
|
2023-10-22 22:20:53 +00:00
|
|
|
}
|
2022-12-19 23:08:11 +00:00
|
|
|
}
|