updated plugin ActivityPub version 8.3.0

This commit is contained in:
2026-06-03 21:28:46 +00:00
committed by Gitium
parent a4b78ec277
commit 6fe182458a
340 changed files with 43232 additions and 7568 deletions

View File

@ -7,8 +7,9 @@
namespace Activitypub;
use WP_Error;
use Activitypub\Activity\Actor;
use Activitypub\Collection\Actors;
use Activitypub\Collection\Remote_Actors;
/**
* ActivityPub WebFinger Class.
@ -18,6 +19,30 @@ use Activitypub\Collection\Actors;
* @see https://webfinger.net/
*/
class Webfinger {
/**
* Check whether a value looks like an `acct` identifier.
*
* Accepts any of:
*
* - `user@host` — bare WebFinger handle.
* - `@user@host` — Mastodon display form with a leading `@`.
* - `acct:user@host` — full RFC 7565 URI form.
*
* The host/local-part pattern follows `ACTIVITYPUB_USERNAME_REGEXP`.
*
* @since 8.3.0
*
* @param mixed $value The candidate value.
* @return bool True if the value matches the acct identifier pattern.
*/
public static function is_acct( $value ) {
if ( ! \is_string( $value ) || '' === $value ) {
return false;
}
return (bool) \preg_match( '/^(?:acct:)?@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $value );
}
/**
* Returns a users WebFinger "resource".
*
@ -39,7 +64,7 @@ class Webfinger {
*
* @param string $uri The WebFinger Resource.
*
* @return string|WP_Error The URL or WP_Error.
* @return string|\WP_Error The URL or WP_Error.
*/
public static function resolve( $uri ) {
$data = self::get_data( $uri );
@ -49,7 +74,7 @@ class Webfinger {
}
if ( ! is_array( $data ) || empty( $data['links'] ) ) {
return new WP_Error(
return new \WP_Error(
'webfinger_missing_links',
__( 'No valid Link elements found.', 'activitypub' ),
array(
@ -72,7 +97,7 @@ class Webfinger {
}
}
return new WP_Error(
return new \WP_Error(
'webfinger_url_no_activitypub',
__( 'The Site supports WebFinger but not ActivityPub', 'activitypub' ),
array(
@ -89,7 +114,7 @@ class Webfinger {
*
* @param string $uri The URI (acct:, mailto:, http:, https:).
*
* @return string|WP_Error Error or acct URI.
* @return string|\WP_Error Error or acct URI.
*/
public static function uri_to_acct( $uri ) {
$data = self::get_data( $uri );
@ -115,7 +140,7 @@ class Webfinger {
}
}
return new WP_Error(
return new \WP_Error(
'webfinger_url_no_acct',
__( 'No acct URI found.', 'activitypub' ),
array(
@ -131,11 +156,11 @@ class Webfinger {
*
* @param string $url The URI (acct:, mailto:, http:, https:).
*
* @return WP_Error|array Error reaction or array with identifier and host as values.
* @return \WP_Error|array Error reaction or array with identifier and host as values.
*/
public static function get_identifier_and_host( $url ) {
if ( ! $url ) {
return new WP_Error(
return new \WP_Error(
'webfinger_invalid_identifier',
__( 'Invalid Identifier', 'activitypub' ),
array(
@ -172,7 +197,7 @@ class Webfinger {
}
if ( empty( $host ) ) {
return new WP_Error(
return new \WP_Error(
'webfinger_invalid_identifier',
__( 'Invalid Identifier', 'activitypub' ),
array(
@ -190,7 +215,7 @@ class Webfinger {
*
* @param string $uri The Identifier: <identifier>@<host> or URI.
*
* @return WP_Error|array Error reaction or array with identifier and host as values.
* @return \WP_Error|array Error reaction or array with identifier and host as values.
*/
public static function get_data( $uri ) {
$identifier_and_host = self::get_identifier_and_host( $uri );
@ -199,45 +224,28 @@ class Webfinger {
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;
}
$webfinger_url = sprintf(
'https://%s/.well-known/webfinger?resource=%s',
$host,
rawurlencode( $identifier )
\rawurlencode( $identifier )
);
$response = wp_safe_remote_get(
// Use Http::get() which handles all caching (success and errors).
$response = Http::get(
$webfinger_url,
array(
'headers' => array( 'Accept' => 'application/jrd+json' ),
)
array( 'headers' => array( 'Accept' => 'application/jrd+json' ) ),
WEEK_IN_SECONDS
);
if ( is_wp_error( $response ) ) {
return new WP_Error(
'webfinger_url_not_accessible',
__( 'The WebFinger Resource is not accessible.', 'activitypub' ),
array(
'status' => 400,
'data' => $webfinger_url,
)
);
if ( \is_wp_error( $response ) ) {
return $response;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
$body = \wp_remote_retrieve_body( $response );
\set_transient( $transient_key, $data, WEEK_IN_SECONDS );
return $data;
return \json_decode( $body, true );
}
/**
@ -245,40 +253,10 @@ class Webfinger {
*
* @param string $uri The WebFinger Resource URI.
*
* @return string|WP_Error Error or the Remote-Follow endpoint URI.
* @return string|\WP_Error Error or the Remote-Follow endpoint URI.
*/
public static function get_remote_follow_endpoint( $uri ) {
$data = self::get_data( $uri );
if ( is_wp_error( $data ) ) {
return $data;
}
if ( empty( $data['links'] ) ) {
return new WP_Error(
'webfinger_missing_links',
__( 'No valid Link elements found.', 'activitypub' ),
array(
'status' => 400,
'data' => $data,
)
);
}
foreach ( $data['links'] as $link ) {
if ( 'http://ostatus.org/schema/1.0/subscribe' === $link['rel'] ) {
return $link['template'];
}
}
return new WP_Error(
'webfinger_missing_remote_follow_endpoint',
__( 'No valid Remote-Follow endpoint found.', 'activitypub' ),
array(
'status' => 400,
'data' => $data,
)
);
return self::get_intent_endpoint( $uri, 'follow', true );
}
/**
@ -297,4 +275,128 @@ class Webfinger {
return 'webfinger_' . md5( $uri );
}
/**
* Infer a shortname from the Actor ID or URL. Used only for fallbacks,
* we will try to use what's supplied.
*
* @param Actor|string $actor_or_uri The Actor or URI.
*
* @return string Hopefully the name of the Follower.
*/
public static function guess( $actor_or_uri ) {
if ( ! $actor_or_uri instanceof Actor ) {
$actor = Remote_Actors::fetch_by_uri( $actor_or_uri );
if ( \is_wp_error( $actor ) ) {
return extract_name_from_uri( $actor_or_uri ) . '@' . \wp_parse_url( $actor_or_uri, PHP_URL_HOST );
}
$actor_or_uri = $actor;
}
if ( $actor_or_uri->get_preferred_username() ) {
return $actor_or_uri->get_preferred_username() . '@' . \wp_parse_url( $actor_or_uri->get_id(), PHP_URL_HOST );
}
return extract_name_from_uri( $actor_or_uri->get_id() ) . '@' . \wp_parse_url( $actor_or_uri->get_id(), PHP_URL_HOST );
}
/**
* Get the Intent endpoint for a given URI and intent.
*
* @since 8.0.0
*
* @see https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md
*
* @param string $uri The WebFinger Resource URI.
* @param string $intent The intent to look for.
* @param bool $fallback Whether to fallback to the Remote-Follow endpoint.
*
* @return string|\WP_Error Error or the Intent endpoint URI (may contain `{uri}` placeholder).
*/
public static function get_intent_endpoint( $uri, $intent, $fallback = false ) {
$data = self::get_data( $uri );
if ( \is_wp_error( $data ) ) {
return $data;
}
if ( empty( $data['links'] ) ) {
return new \WP_Error(
'webfinger_missing_links',
\__( 'No valid Link elements found.', 'activitypub' ),
array(
'status' => 400,
'data' => $data,
)
);
}
// Normalize the links with $rel as key.
$links = array();
foreach ( $data['links'] as $link ) {
if ( isset( $link['rel'] ) && isset( $link['template'] ) ) {
$links[ \strtolower( $link['rel'] ) ] = $link['template'];
}
}
$intent = \sanitize_text_field( $intent );
$intent = \strtolower( $intent );
if ( ! \filter_var( $intent, FILTER_VALIDATE_URL ) ) {
$intent = 'https://w3id.org/fep/3b86/' . $intent;
}
if ( isset( $links[ $intent ] ) ) {
return $links[ $intent ];
}
if ( ! $fallback ) {
return new \WP_Error(
'webfinger_missing_intent_endpoint',
\__( 'No valid Intent endpoint found.', 'activitypub' ),
array(
'status' => 400,
'data' => $data,
)
);
}
/*
* OStatus subscribe URL (deprecated but still widely supported)
*
* @see https://ostatus.github.io/spec/OStatus%201.0%20Draft%202.html#anchor10
*/
if ( isset( $links['http://ostatus.org/schema/1.0/subscribe'] ) ) {
return $links['http://ostatus.org/schema/1.0/subscribe'];
}
/*
* FEP-3b86 Object Intent — the generic "open this object on my home
* server" link, equivalent to pasting the URL into the home server's
* search box. Useful when no verb-specific intent is advertised.
*
* @see https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md#5-1-object-intent
*/
if ( isset( $links['https://w3id.org/fep/3b86/object'] ) ) {
return $links['https://w3id.org/fep/3b86/object'];
}
// Last-resort: construct a Mastodon-compatible authorize_interaction URL.
$identifier_and_host = self::get_identifier_and_host( $uri );
if ( \is_wp_error( $identifier_and_host ) ) {
return new \WP_Error(
'webfinger_missing_intent_endpoint',
\__( 'No valid Intent endpoint found.', 'activitypub' ),
array(
'status' => 400,
'data' => $data,
)
);
}
return 'https://' . $identifier_and_host[1] . '/authorize_interaction?uri={uri}';
}
}