updated plugin ActivityPub version 3.3.3

This commit is contained in:
2024-10-09 12:44:17 +00:00
committed by Gitium
parent fb4b27bbc6
commit c54fa007bd
106 changed files with 7070 additions and 2918 deletions

View File

@ -1,18 +1,22 @@
<?php
/**
* ActivityPub Actors REST-Class
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_Error;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use Activitypub\Webfinger;
use Activitypub\Activity\Activity;
use Activitypub\Collection\Users as User_Collection;
use function Activitypub\is_activitypub_request;
/**
* ActivityPub Actors REST-Class
* ActivityPub Actors REST-Class.
*
* @author Matthias Pfefferle
*
@ -20,14 +24,14 @@ use function Activitypub\is_activitypub_request;
*/
class Actors {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
}
/**
* Register routes
* Register routes.
*/
public static function register_routes() {
\register_rest_route(
@ -65,9 +69,9 @@ class Actors {
/**
* Handle GET request
*
* @param WP_REST_Request $request
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function get( $request ) {
$user_id = $request->get_param( 'user_id' );
@ -77,14 +81,17 @@ class Actors {
return $user;
}
// redirect to canonical URL if it is not an ActivityPub request
$link_header = sprintf( '<%1$s>; rel="alternate"; type="application/activity+json"', $user->get_id() );
// Redirect to canonical URL if it is not an ActivityPub request.
if ( ! is_activitypub_request() ) {
header( 'Link: ' . $link_header );
header( 'Location: ' . $user->get_canonical_url(), true, 301 );
exit;
}
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*/
\do_action( 'activitypub_rest_users_pre' );
@ -92,17 +99,18 @@ class Actors {
$rest_response = new WP_REST_Response( $json, 200 );
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
$rest_response->header( 'Link', $link_header );
return $rest_response;
}
/**
* Endpoint for remote follow UI/Block
* Endpoint for remote follow UI/Block.
*
* @param WP_REST_Request $request The request object.
*
* @return void|string The URL to the remote follow page
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function remote_follow_get( WP_REST_Request $request ) {
$resource = $request->get_param( 'resource' );
@ -123,15 +131,18 @@ class Actors {
$url = str_replace( '{uri}', $resource, $template );
return new WP_REST_Response(
array( 'url' => $url, 'template' => $template ),
array(
'url' => $url,
'template' => $template,
),
200
);
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters,
*/
public static function request_parameters() {
$params = array();

View File

@ -1,4 +1,10 @@
<?php
/**
* Collections REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_REST_Server;
@ -6,14 +12,17 @@ use WP_REST_Response;
use Activitypub\Activity\Actor;
use Activitypub\Activity\Base_Object;
use Activitypub\Collection\Users as User_Collection;
use Activitypub\Collection\Replies;
use Activitypub\Transformer\Factory;
use WP_Error;
use function Activitypub\esc_hashtag;
use function Activitypub\is_single_user;
use function Activitypub\get_rest_url_by_path;
/**
* ActivityPub Collections REST-Class
* ActivityPub Collections REST-Class.
*
* @author Matthias Pfefferle
*
@ -22,14 +31,14 @@ use function Activitypub\get_rest_url_by_path;
*/
class Collection {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
}
/**
* Register routes
* Register routes.
*/
public static function register_routes() {
\register_rest_route(
@ -69,14 +78,81 @@ class Collection {
),
)
);
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/(?P<type>[\w\-\.]+)s/(?P<id>[\w\-\.]+)/replies',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( self::class, 'replies_get' ),
'args' => self::request_parameters_for_replies(),
'permission_callback' => '__return_true',
),
)
);
}
/**
* The endpoint for replies collections.
*
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function replies_get( $request ) {
$type = $request->get_param( 'type' );
// Get the WordPress object of that "owns" the requested replies.
switch ( $type ) {
case 'comment':
$wp_object = \get_comment( $request->get_param( 'id' ) );
break;
case 'post':
default:
$wp_object = \get_post( $request->get_param( 'id' ) );
break;
}
if ( ! isset( $wp_object ) || is_wp_error( $wp_object ) ) {
return new WP_Error(
'activitypub_replies_collection_does_not_exist',
\sprintf(
// translators: %s: The type (post, comment, etc.) for which no replies collection exists.
\__( 'No reply collection exists for the type %s.', 'activitypub' ),
$type
)
);
}
$page = intval( $request->get_param( 'page' ) );
// If the request parameter page is present get the CollectionPage otherwise the replies collection.
if ( isset( $page ) ) {
$response = Replies::get_collection_page( $wp_object, $page );
} else {
$response = Replies::get_collection( $wp_object );
}
if ( is_wp_error( $response ) ) {
return $response;
}
// Add ActivityPub Context.
$response = array_merge(
array( '@context' => Base_Object::JSON_LD_CONTEXT ),
$response
);
return new WP_REST_Response( $response, 200 );
}
/**
* The Featured Tags endpoint
*
* @param WP_REST_Request $request The request object.
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response object.
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function tags_get( $request ) {
$user_id = $request->get_param( 'user_id' );
@ -126,9 +202,9 @@ class Collection {
/**
* Featured posts endpoint
*
* @param WP_REST_Request $request The request object.
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response object.
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function featured_get( $request ) {
$user_id = $request->get_param( 'user_id' );
@ -184,13 +260,11 @@ class Collection {
}
/**
* Moderators endpoint
*
* @param WP_REST_Request $request The request object.
* Moderators endpoint.
*
* @return WP_REST_Response The response object.
*/
public static function moderators_get( $request ) {
public static function moderators_get() {
$response = array(
'@context' => Actor::JSON_LD_CONTEXT,
'id' => get_rest_url_by_path( 'collections/moderators' ),
@ -211,16 +285,38 @@ class Collection {
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function request_parameters() {
$params = array();
$params['user_id'] = array(
'required' => true,
'type' => 'string',
'type' => 'string',
);
return $params;
}
/**
* The supported parameters.
*
* @return array list of parameters.
*/
public static function request_parameters_for_replies() {
$params = array();
$params['type'] = array(
'required' => true,
'type' => 'string',
'enum' => array( 'post', 'comment' ),
);
$params['id'] = array(
'required' => true,
'type' => 'string',
);
return $params;

View File

@ -1,4 +1,10 @@
<?php
/**
* Comment REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_Error;
@ -9,7 +15,7 @@ use Activitypub\Comment as Comment_Utils;
use Activitypub\Webfinger as Webfinger_Utils;
/**
* ActivityPub Followers REST-Class
* ActivityPub Followers REST-Class.
*
* @author Matthias Pfefferle
*
@ -17,14 +23,14 @@ use Activitypub\Webfinger as Webfinger_Utils;
*/
class Comment {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
}
/**
* Register routes
* Register routes.
*/
public static function register_routes() {
\register_rest_route(
@ -47,11 +53,11 @@ class Comment {
}
/**
* Endpoint for remote follow UI/Block
* Endpoint for remote follow UI/Block.
*
* @param WP_REST_Request $request The request object.
*
* @return void|string The URL to the remote follow page
* @return array|string|WP_Error|WP_REST_Response The URL to the remote follow page or an error.
*/
public static function remote_reply_get( WP_REST_Request $request ) {
$resource = $request->get_param( 'resource' );
@ -75,20 +81,19 @@ class Comment {
return $template;
}
$comment_meta = \get_comment_meta( $comment_id );
$resource = Comment_Utils::get_source_id( $comment_id );
if ( ! empty( $comment_meta['source_id'][0] ) ) {
$resource = $comment_meta['source_id'][0];
} elseif ( ! empty( $comment_meta['source_url'][0] ) ) {
$resource = $comment_meta['source_url'][0];
} else {
if ( ! $resource ) {
$resource = Comment_Utils::generate_id( $comment );
}
$url = str_replace( '{uri}', $resource, $template );
return new WP_REST_Response(
array( 'url' => $url, 'template' => $template ),
array(
'url' => $url,
'template' => $template,
),
200
);
}

View File

@ -1,7 +1,12 @@
<?php
/**
* Followers REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_Error;
use stdClass;
use WP_REST_Server;
use WP_REST_Response;
@ -12,7 +17,7 @@ use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_masked_wp_version;
/**
* ActivityPub Followers REST-Class
* ActivityPub Followers REST-Class.
*
* @author Matthias Pfefferle
*
@ -20,14 +25,14 @@ use function Activitypub\get_masked_wp_version;
*/
class Followers {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
}
/**
* Register routes
* Register routes.
*/
public static function register_routes() {
\register_rest_route(
@ -47,9 +52,9 @@ class Followers {
/**
* Handle GET request
*
* @param WP_REST_Request $request
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function get( $request ) {
$user_id = $request->get_param( 'user_id' );
@ -64,36 +69,34 @@ class Followers {
$page = (int) $request->get_param( 'page' );
$context = $request->get_param( 'context' );
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client
*/
\do_action( 'activitypub_rest_followers_pre' );
$data = Follower_Collection::get_followers_with_count( $user_id, $per_page, $page, array( 'order' => ucwords( $order ) ) );
$json = new stdClass();
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$json->{'@context'} = \Activitypub\get_context();
$json->id = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user->get__id() ) );
$json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version();
$json->actor = $user->get_id();
$json->type = 'OrderedCollectionPage';
$json->totalItems = $data['total'];
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user->get__id() ) );
$json->id = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user->get__id() ) );
$json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version();
$json->actor = $user->get_id();
$json->type = 'OrderedCollectionPage';
$json->first = \add_query_arg( 'page', 1, $json->partOf );
$json->last = \add_query_arg( 'page', \ceil( $json->totalItems / $per_page ), $json->partOf );
$json->totalItems = $data['total']; // phpcs:ignore
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user->get__id() ) ); // phpcs:ignore
$json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore
$json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / $per_page ), $json->partOf ); // phpcs:ignore
if ( $page && ( ( \ceil ( $json->totalItems / $per_page ) ) > $page ) ) { // phpcs:ignore
$json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore
if ( $page && ( ( \ceil( $json->totalItems / $per_page ) ) > $page ) ) {
$json->next = \add_query_arg( 'page', $page + 1, $json->partOf );
}
if ( $page && ( $page > 1 ) ) { // phpcs:ignore
$json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); // phpcs:ignore
if ( $page && ( $page > 1 ) ) {
$json->prev = \add_query_arg( 'page', $page - 1, $json->partOf );
}
// phpcs:ignore
$json->orderedItems = array_map(
function ( $item ) use ( $context ) {
if ( 'full' === $context ) {
@ -103,6 +106,7 @@ class Followers {
},
$data['followers']
);
// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$rest_response = new WP_REST_Response( $json, 200 );
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
@ -111,20 +115,20 @@ class Followers {
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function request_parameters() {
$params = array();
$params['page'] = array(
'type' => 'integer',
'type' => 'integer',
'default' => 1,
);
$params['per_page'] = array(
'type' => 'integer',
'type' => 'integer',
'default' => 20,
);
@ -136,13 +140,13 @@ class Followers {
$params['user_id'] = array(
'required' => true,
'type' => 'string',
'type' => 'string',
);
$params['context'] = array(
'type' => 'string',
'type' => 'string',
'default' => 'simple',
'enum' => array( 'simple', 'full' ),
'enum' => array( 'simple', 'full' ),
);
return $params;

View File

@ -1,4 +1,10 @@
<?php
/**
* ActivityPub Following REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_REST_Response;
@ -9,7 +15,7 @@ use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_masked_wp_version;
/**
* ActivityPub Following REST-Class
* ActivityPub Following REST-Class.
*
* @author Matthias Pfefferle
*
@ -17,7 +23,7 @@ use function Activitypub\get_masked_wp_version;
*/
class Following {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
@ -46,9 +52,9 @@ class Following {
/**
* Handle GET request
*
* @param WP_REST_Request $request
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function get( $request ) {
$user_id = $request->get_param( 'user_id' );
@ -58,8 +64,8 @@ class Following {
return $user;
}
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*/
\do_action( 'activitypub_rest_following_pre' );
@ -67,19 +73,25 @@ class Following {
$json->{'@context'} = \Activitypub\get_context();
$json->id = get_rest_url_by_path( sprintf( 'actors/%d/following', $user->get__id() ) );
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$json->id = get_rest_url_by_path( sprintf( 'actors/%d/following', $user->get__id() ) );
$json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version();
$json->actor = $user->get_id();
$json->type = 'OrderedCollectionPage';
$json->actor = $user->get_id();
$json->type = 'OrderedCollectionPage';
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/following', $user->get__id() ) );
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/following', $user->get__id() ) ); // phpcs:ignore
/**
* Filter the list of following urls.
*
* @param array $items The array of following urls.
* @param \Activitypub\Model\User $user The user object.
*/
$items = apply_filters( 'activitypub_rest_following', array(), $user );
$items = apply_filters( 'activitypub_rest_following', array(), $user ); // phpcs:ignore
$json->totalItems = is_countable( $items ) ? count( $items ) : 0; // phpcs:ignore
$json->orderedItems = $items; // phpcs:ignore
$json->first = $json->partOf; // phpcs:ignore
$json->totalItems = is_countable( $items ) ? count( $items ) : 0;
$json->orderedItems = $items;
$json->first = $json->partOf;
// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$rest_response = new WP_REST_Response( $json, 200 );
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
@ -88,9 +100,9 @@ class Following {
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function request_parameters() {
$params = array();
@ -101,7 +113,7 @@ class Following {
$params['user_id'] = array(
'required' => true,
'type' => 'string',
'type' => 'string',
);
return $params;
@ -111,22 +123,22 @@ class Following {
* Add the Blog Authors to the following list of the Blog Actor
* if Blog not in single mode.
*
* @param array $array The array of following urls.
* @param User $user The user object.
* @param array $follow_list The array of following urls.
* @param \Activitypub\Model\User $user The user object.
*
* @return array The array of following urls.
*/
public static function default_following( $array, $user ) {
public static function default_following( $follow_list, $user ) {
if ( 0 !== $user->get__id() || is_single_user() ) {
return $array;
return $follow_list;
}
$users = User_Collection::get_collection();
foreach ( $users as $user ) {
$array[] = $user->get_url();
$follow_list[] = $user->get_url();
}
return $array;
return $follow_list;
}
}

View File

@ -1,21 +1,25 @@
<?php
/**
* Inbox REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_Error;
use WP_REST_Server;
use WP_REST_Response;
use Activitypub\Activity\Activity;
use Activitypub\Collection\Users as User_Collection;
use function Activitypub\get_context;
use function Activitypub\object_to_uri;
use function Activitypub\url_to_authorid;
use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_masked_wp_version;
use function Activitypub\extract_recipients_from_activity;
/**
* ActivityPub Inbox REST-Class
* ActivityPub Inbox REST-Class.
*
* @author Matthias Pfefferle
*
@ -23,14 +27,14 @@ use function Activitypub\extract_recipients_from_activity;
*/
class Inbox {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
}
/**
* Register routes
* Register routes.
*/
public static function register_routes() {
\register_rest_route(
@ -67,10 +71,10 @@ class Inbox {
}
/**
* Renders the user-inbox
* Renders the user-inbox.
*
* @param WP_REST_Request $request
* @return WP_REST_Response
* @param \WP_REST_Request $request The request object.
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function user_inbox_get( $request ) {
$user_id = $request->get_param( 'user_id' );
@ -80,29 +84,33 @@ class Inbox {
return $user;
}
$page = $request->get_param( 'page', 0 );
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*/
\do_action( 'activitypub_rest_inbox_pre' );
$json = new \stdClass();
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$json->{'@context'} = get_context();
$json->id = get_rest_url_by_path( sprintf( 'actors/%d/inbox', $user->get__id() ) );
$json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version();
$json->type = 'OrderedCollectionPage';
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/inbox', $user->get__id() ) ); // phpcs:ignore
$json->totalItems = 0; // phpcs:ignore
$json->orderedItems = array(); // phpcs:ignore
$json->first = $json->partOf; // phpcs:ignore
$json->id = get_rest_url_by_path( sprintf( 'actors/%d/inbox', $user->get__id() ) );
$json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version();
$json->type = 'OrderedCollectionPage';
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/inbox', $user->get__id() ) );
$json->totalItems = 0;
$json->orderedItems = array();
$json->first = $json->partOf;
// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// filter output
/**
* Filter the ActivityPub inbox array.
*
* @param array $json The ActivityPub inbox array.
*/
$json = \apply_filters( 'activitypub_rest_inbox_array', $json );
/*
* Action triggerd after the ActivityPub profile has been created and sent to the client
/**
* Action triggered after the ActivityPub profile has been created and sent to the client.
*/
\do_action( 'activitypub_inbox_post' );
@ -113,11 +121,11 @@ class Inbox {
}
/**
* Handles user-inbox requests
* Handles user-inbox requests.
*
* @param WP_REST_Request $request
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function user_inbox_post( $request ) {
$user_id = $request->get_param( 'user_id' );
@ -132,7 +140,23 @@ class Inbox {
$type = $request->get_param( 'type' );
$type = \strtolower( $type );
/**
* ActivityPub inbox action.
*
* @param array $data The data array.
* @param int|null $user_id The user ID.
* @param string $type The type of the activity.
* @param Activity $activity The Activity object.
*/
\do_action( 'activitypub_inbox', $data, $user->get__id(), $type, $activity );
/**
* ActivityPub inbox action for specific activity types.
*
* @param array $data The data array.
* @param int|null $user_id The user ID.
* @param Activity $activity The Activity object.
*/
\do_action( "activitypub_inbox_{$type}", $data, $user->get__id(), $activity );
$rest_response = new WP_REST_Response( array(), 202 );
@ -142,9 +166,9 @@ class Inbox {
}
/**
* The shared inbox
* The shared inbox.
*
* @param WP_REST_Request $request
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response
*/
@ -154,7 +178,23 @@ class Inbox {
$type = $request->get_param( 'type' );
$type = \strtolower( $type );
/**
* ActivityPub inbox action.
*
* @param array $data The data array.
* @param int|null $user_id The user ID.
* @param string $type The type of the activity.
* @param Activity $activity The Activity object.
*/
\do_action( 'activitypub_inbox', $data, null, $type, $activity );
/**
* ActivityPub inbox action for specific activity types.
*
* @param array $data The data array.
* @param int|null $user_id The user ID.
* @param Activity $activity The Activity object.
*/
\do_action( "activitypub_inbox_{$type}", $data, null, $activity );
$rest_response = new WP_REST_Response( array(), 202 );
@ -164,9 +204,9 @@ class Inbox {
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function user_inbox_get_parameters() {
$params = array();
@ -177,100 +217,68 @@ class Inbox {
$params['user_id'] = array(
'required' => true,
'type' => 'string',
'type' => 'string',
);
return $params;
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function user_inbox_post_parameters() {
$params = array();
$params['page'] = array(
'type' => 'integer',
);
$params['user_id'] = array(
'required' => true,
'type' => 'string',
'type' => 'string',
);
$params['id'] = array(
'required' => true,
'required' => true,
'sanitize_callback' => 'esc_url_raw',
);
$params['actor'] = array(
'required' => true,
'sanitize_callback' => function ( $param, $request, $key ) {
return object_to_uri( $param );
},
'required' => true,
'sanitize_callback' => '\Activitypub\object_to_uri',
);
$params['type'] = array(
'required' => true,
//'type' => 'enum',
//'enum' => array( 'Create' ),
//'sanitize_callback' => function ( $param, $request, $key ) {
// return \strtolower( $param );
//},
);
$params['object'] = array(
'required' => true,
'required' => true,
'validate_callback' => function ( $param, $request, $key ) {
/**
* Filter the ActivityPub object validation.
*
* @param bool $validate The validation result.
* @param array $param The object data.
* @param object $request The request object.
* @param string $key The key.
*/
return apply_filters( 'activitypub_validate_object', true, $param, $request, $key );
},
);
return $params;
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function shared_inbox_post_parameters() {
$params = array();
$params['page'] = array(
'type' => 'integer',
);
$params['id'] = array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'esc_url_raw',
);
$params['actor'] = array(
'required' => true,
//'type' => array( 'object', 'string' ),
'sanitize_callback' => function ( $param, $request, $key ) {
return object_to_uri( $param );
},
);
$params['type'] = array(
'required' => true,
//'type' => 'enum',
//'enum' => array( 'Create' ),
//'sanitize_callback' => function ( $param, $request, $key ) {
// return \strtolower( $param );
//},
);
$params['object'] = array(
'required' => true,
//'type' => 'object',
);
$params = self::user_inbox_post_parameters();
$params['to'] = array(
'required' => false,
'sanitize_callback' => function ( $param, $request, $key ) {
'required' => false,
'sanitize_callback' => function ( $param ) {
if ( \is_string( $param ) ) {
$param = array( $param );
}
@ -280,7 +288,7 @@ class Inbox {
);
$params['cc'] = array(
'sanitize_callback' => function ( $param, $request, $key ) {
'sanitize_callback' => function ( $param ) {
if ( \is_string( $param ) ) {
$param = array( $param );
}
@ -290,7 +298,7 @@ class Inbox {
);
$params['bcc'] = array(
'sanitize_callback' => function ( $param, $request, $key ) {
'sanitize_callback' => function ( $param ) {
if ( \is_string( $param ) ) {
$param = array( $param );
}
@ -303,15 +311,15 @@ class Inbox {
}
/**
* Get local user recipients
* Get local user recipients.
*
* @param array $data
* @param array $data The data array.
*
* @return array The list of local users
* @return array The list of local users.
*/
public static function get_recipients( $data ) {
$recipients = extract_recipients_from_activity( $data );
$users = array();
$users = array();
foreach ( $recipients as $recipient ) {
$user_id = url_to_authorid( $recipient );

View File

@ -0,0 +1,118 @@
<?php
/**
* ActivityPub Interaction REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_REST_Response;
use Activitypub\Http;
/**
* Interaction class.
*/
class Interaction {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
}
/**
* Register routes
*/
public static function register_routes() {
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/interactions',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( self::class, 'get' ),
'permission_callback' => '__return_true',
'args' => array(
'uri' => array(
'type' => 'string',
'required' => true,
'sanitize_callback' => 'esc_url',
),
),
),
)
);
}
/**
* Handle GET request.
*
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response Redirect to the editor or die.
*/
public static function get( $request ) {
$uri = $request->get_param( 'uri' );
$redirect_url = null;
$object = Http::get_remote_object( $uri );
if (
\is_wp_error( $object ) ||
! isset( $object['type'] )
) {
\wp_die(
\esc_html__(
'The URL is not supported!',
'activitypub'
),
400
);
}
if ( ! empty( $object['url'] ) ) {
$uri = \esc_url( $object['url'] );
}
switch ( $object['type'] ) {
case 'Group':
case 'Person':
case 'Service':
case 'Application':
case 'Organization':
$redirect_url = \apply_filters( 'activitypub_interactions_follow_url', $redirect_url, $uri, $object );
break;
default:
$redirect_url = \admin_url( 'post-new.php?in_reply_to=' . $uri );
$redirect_url = \apply_filters( 'activitypub_interactions_reply_url', $redirect_url, $uri, $object );
}
/**
* Filter the redirect URL.
*
* @param string $redirect_url The URL to redirect to.
* @param string $uri The URI of the object.
* @param array $object The object.
*/
$redirect_url = \apply_filters( 'activitypub_interactions_url', $redirect_url, $uri, $object );
// Check if hook is implemented.
if ( ! $redirect_url ) {
\wp_die(
esc_html__(
'This Interaction type is not supported yet!',
'activitypub'
),
400
);
}
return new WP_REST_Response(
null,
302,
array(
'Location' => \esc_url( $redirect_url ),
)
);
}
}

View File

@ -1,4 +1,10 @@
<?php
/**
* NodeInfo REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_REST_Response;
@ -9,7 +15,7 @@ use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_masked_wp_version;
/**
* ActivityPub NodeInfo REST-Class
* ActivityPub NodeInfo REST-Class.
*
* @author Matthias Pfefferle
*
@ -17,7 +23,7 @@ use function Activitypub\get_masked_wp_version;
*/
class Nodeinfo {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
@ -65,97 +71,93 @@ class Nodeinfo {
}
/**
* Render NodeInfo file
* Render NodeInfo file.
*
* @param WP_REST_Request $request
*
* @return WP_REST_Response
* @return WP_REST_Response The JSON profile of the NodeInfo.
*/
public static function nodeinfo( $request ) {
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
public static function nodeinfo() {
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*/
\do_action( 'activitypub_rest_nodeinfo_pre' );
$nodeinfo = array();
$nodeinfo['version'] = '2.0';
$nodeinfo['version'] = '2.0';
$nodeinfo['software'] = array(
'name' => 'wordpress',
'name' => 'wordpress',
'version' => get_masked_wp_version(),
);
$posts = \wp_count_posts();
$posts = \wp_count_posts();
$comments = \wp_count_comments();
$nodeinfo['usage'] = array(
'users' => array(
'users' => array(
'total' => get_total_users(),
'activeMonth' => get_active_users( '1 month ago' ),
'activeHalfyear' => get_active_users( '6 month ago' ),
),
'localPosts' => (int) $posts->publish,
'localPosts' => (int) $posts->publish,
'localComments' => (int) $comments->approved,
);
$nodeinfo['openRegistrations'] = false;
$nodeinfo['protocols'] = array( 'activitypub' );
$nodeinfo['protocols'] = array( 'activitypub' );
$nodeinfo['services'] = array(
'inbound' => array(),
'inbound' => array(),
'outbound' => array(),
);
$nodeinfo['metadata'] = array(
'nodeName' => \get_bloginfo( 'name' ),
'nodeName' => \get_bloginfo( 'name' ),
'nodeDescription' => \get_bloginfo( 'description' ),
'nodeIcon' => \get_site_icon_url(),
'nodeIcon' => \get_site_icon_url(),
);
return new WP_REST_Response( $nodeinfo, 200 );
}
/**
* Render NodeInfo file
* Render NodeInfo file.
*
* @param WP_REST_Request $request
*
* @return WP_REST_Response
* @return WP_REST_Response The JSON profile of the NodeInfo.
*/
public static function nodeinfo2( $request ) {
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
public static function nodeinfo2() {
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*/
\do_action( 'activitypub_rest_nodeinfo2_pre' );
$nodeinfo = array();
$nodeinfo['version'] = '2.0';
$nodeinfo['server'] = array(
'baseUrl' => \home_url( '/' ),
'name' => \get_bloginfo( 'name' ),
$nodeinfo['server'] = array(
'baseUrl' => \home_url( '/' ),
'name' => \get_bloginfo( 'name' ),
'software' => 'wordpress',
'version' => get_masked_wp_version(),
'version' => get_masked_wp_version(),
);
$posts = \wp_count_posts();
$posts = \wp_count_posts();
$comments = \wp_count_comments();
$nodeinfo['usage'] = array(
'users' => array(
'users' => array(
'total' => get_total_users(),
'activeMonth' => get_active_users( 1 ),
'activeHalfyear' => get_active_users( 6 ),
),
'localPosts' => (int) $posts->publish,
'localPosts' => (int) $posts->publish,
'localComments' => (int) $comments->approved,
);
$nodeinfo['openRegistrations'] = false;
$nodeinfo['protocols'] = array( 'activitypub' );
$nodeinfo['protocols'] = array( 'activitypub' );
$nodeinfo['services'] = array(
'inbound' => array(),
'inbound' => array(),
'outbound' => array(),
);
@ -163,21 +165,19 @@ class Nodeinfo {
}
/**
* Render NodeInfo discovery file
*
* @param WP_REST_Request $request
* Render NodeInfo discovery file.
*
* @return WP_REST_Response
*/
public static function discovery( $request ) {
$discovery = array();
public static function discovery() {
$discovery = array();
$discovery['links'] = array(
array(
'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0',
'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0',
'href' => get_rest_url_by_path( 'nodeinfo' ),
),
array(
'rel' => 'https://www.w3.org/ns/activitystreams#Application',
'rel' => 'https://www.w3.org/ns/activitystreams#Application',
'href' => get_rest_url_by_path( 'application' ),
),
);

View File

@ -1,8 +1,13 @@
<?php
/**
* Outbox REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use stdClass;
use WP_Error;
use WP_REST_Server;
use WP_REST_Response;
use Activitypub\Activity\Activity;
@ -14,7 +19,7 @@ use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_masked_wp_version;
/**
* ActivityPub Outbox REST-Class
* ActivityPub Outbox REST-Class.
*
* @author Matthias Pfefferle
*
@ -22,7 +27,7 @@ use function Activitypub\get_masked_wp_version;
*/
class Outbox {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
@ -49,8 +54,8 @@ class Outbox {
/**
* Renders the user-outbox
*
* @param WP_REST_Request $request
* @return WP_REST_Response
* @param \WP_REST_Request $request The request object.
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function user_outbox_get( $request ) {
$user_id = $request->get_param( 'user_id' );
@ -64,41 +69,43 @@ class Outbox {
$page = $request->get_param( 'page', 1 );
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*/
\do_action( 'activitypub_rest_outbox_pre' );
$json = new stdClass();
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$json->{'@context'} = get_context();
$json->id = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) );
$json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version();
$json->actor = $user->get_id();
$json->type = 'OrderedCollectionPage';
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) ); // phpcs:ignore
$json->totalItems = 0; // phpcs:ignore
$json->id = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) );
$json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version();
$json->actor = $user->get_id();
$json->type = 'OrderedCollectionPage';
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) );
$json->totalItems = 0;
if ( $user_id > 0 ) {
$count_posts = \count_user_posts( $user_id, $post_types, true );
$json->totalItems = \intval( $count_posts ); // phpcs:ignore
$count_posts = \count_user_posts( $user_id, $post_types, true );
$json->totalItems = \intval( $count_posts );
} else {
foreach ( $post_types as $post_type ) {
$count_posts = \wp_count_posts( $post_type );
$json->totalItems += \intval( $count_posts->publish ); // phpcs:ignore
$count_posts = \wp_count_posts( $post_type );
$json->totalItems += \intval( $count_posts->publish );
}
}
$json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore
$json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / 10 ), $json->partOf ); // phpcs:ignore
$json->first = \add_query_arg( 'page', 1, $json->partOf );
$json->last = \add_query_arg( 'page', \ceil( $json->totalItems / 10 ), $json->partOf );
if ( $page && ( ( \ceil ( $json->totalItems / 10 ) ) > $page ) ) { // phpcs:ignore
$json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore
if ( $page && ( ( \ceil( $json->totalItems / 10 ) ) > $page ) ) {
$json->next = \add_query_arg( 'page', $page + 1, $json->partOf );
}
if ( $page && ( $page > 1 ) ) { // phpcs:ignore
$json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); // phpcs:ignore
if ( $page && ( $page > 1 ) ) {
$json->prev = \add_query_arg( 'page', $page - 1, $json->partOf );
}
// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
if ( $page ) {
$posts = \get_posts(
@ -121,15 +128,19 @@ class Outbox {
$activity = new Activity();
$activity->set_type( 'Create' );
$activity->set_object( $post );
$json->orderedItems[] = $activity->to_array( false ); // phpcs:ignore
$json->orderedItems[] = $activity->to_array( false ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
}
}
// filter output
/**
* Filter the ActivityPub outbox array.
*
* @param array $json The ActivityPub outbox array.
*/
$json = \apply_filters( 'activitypub_rest_outbox_array', $json );
/*
* Action triggerd after the ActivityPub profile has been created and sent to the client
/**
* Action triggered after the ActivityPub profile has been created and sent to the client
*/
\do_action( 'activitypub_outbox_post' );
@ -140,21 +151,21 @@ class Outbox {
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function request_parameters() {
$params = array();
$params['page'] = array(
'type' => 'integer',
'type' => 'integer',
'default' => 1,
);
$params['user_id'] = array(
'required' => true,
'type' => 'string',
'type' => 'string',
);
return $params;

View File

@ -1,14 +1,19 @@
<?php
/**
* Server REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use stdClass;
use WP_Error;
use WP_REST_Response;
use Activitypub\Signature;
use Activitypub\Model\Application;
/**
* ActivityPub Server REST-Class
* ActivityPub Server REST-Class.
*
* @author Django Doucet
*
@ -16,11 +21,12 @@ use Activitypub\Model\Application;
*/
class Server {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
\add_filter( 'rest_request_before_callbacks', array( self::class, 'validate_activitypub_requests' ), 9, 3 );
\add_filter( 'rest_request_before_callbacks', array( self::class, 'authorize_activitypub_requests' ), 10, 3 );
}
@ -65,10 +71,10 @@ class Server {
* @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
* @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
*
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
* @param WP_REST_Request $request Request used to generate the response.
* @param WP_REST_Response|\WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
* @param \WP_REST_Request $request Request used to generate the response.
*
* @return mixed|WP_Error The response, error, or modified response.
*/
@ -77,9 +83,13 @@ class Server {
return $response;
}
if ( \is_wp_error( $response ) ) {
return $response;
}
$route = $request->get_route();
// check if it is an activitypub request and exclude webfinger and nodeinfo endpoints
// Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints.
if (
! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ||
\str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) ||
@ -90,13 +100,13 @@ class Server {
}
/**
* Filter to defer signature verification
* Filter to defer signature verification.
*
* Skip signature verification for debugging purposes or to reduce load for
* certain Activity-Types, like "Delete".
*
* @param bool $defer Whether to defer signature verification.
* @param WP_REST_Request $request The request used to generate the response.
* @param bool $defer Whether to defer signature verification.
* @param \WP_REST_Request $request The request used to generate the response.
*
* @return bool Whether to defer signature verification.
*/
@ -107,9 +117,9 @@ class Server {
}
if (
// POST-Requests are always signed
// POST-Requests are always signed.
'GET' !== $request->get_method() ||
// GET-Requests only require a signature in secure mode
// GET-Requests only require a signature in secure mode.
( 'GET' === $request->get_method() && ACTIVITYPUB_AUTHORIZED_FETCH )
) {
$verified_request = Signature::verify_http_signature( $request );
@ -124,4 +134,51 @@ class Server {
return $response;
}
/**
* Callback function to validate incoming ActivityPub requests
*
* @param WP_REST_Response|\WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
* @param \WP_REST_Request $request Request used to generate the response.
*
* @return mixed|WP_Error The response, error, or modified response.
*/
public static function validate_activitypub_requests( $response, $handler, $request ) {
if ( 'HEAD' === $request->get_method() ) {
return $response;
}
$route = $request->get_route();
if (
\is_wp_error( $response ) ||
! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE )
) {
return $response;
}
$params = $request->get_json_params();
// Type is required for ActivityPub requests, so it fail later in the process.
if ( ! isset( $params['type'] ) ) {
return $response;
}
if (
ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS &&
in_array( $params['type'], array( 'Create', 'Like', 'Announce' ), true )
) {
return new WP_Error(
'activitypub_server_does_not_accept_incoming_interactions',
\__( 'This server does not accept incoming interactions.', 'activitypub' ),
// We have to use a 2XX status code here, because otherwise the response will be
// treated as an error and Mastodon might block this WordPress instance.
array( 'status' => 202 )
);
}
return $response;
}
}

View File

@ -1,12 +1,16 @@
<?php
/**
* WebFinger REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_Error;
use WP_REST_Response;
use Activitypub\Collection\Users as User_Collection;
/**
* ActivityPub WebFinger REST-Class
* ActivityPub WebFinger REST-Class.
*
* @author Matthias Pfefferle
*
@ -15,8 +19,6 @@ use Activitypub\Collection\Users as User_Collection;
class Webfinger {
/**
* Initialize the class, registering WordPress hooks.
*
* @return void
*/
public static function init() {
self::register_routes();
@ -24,8 +26,6 @@ class Webfinger {
/**
* Register routes.
*
* @return void
*/
public static function register_routes() {
\register_rest_route(
@ -45,13 +45,13 @@ class Webfinger {
/**
* WebFinger endpoint.
*
* @param WP_REST_Request $request The request object.
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response object.
*/
public static function webfinger( $request ) {
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*/
\do_action( 'activitypub_rest_webfinger_pre' );
@ -74,13 +74,13 @@ class Webfinger {
$code,
array(
'Access-Control-Allow-Origin' => '*',
'Content-Type' => 'application/jrd+json; charset=' . get_option( 'blog_charset' ),
'Content-Type' => 'application/jrd+json; charset=' . get_option( 'blog_charset' ),
)
);
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
*/
@ -89,8 +89,8 @@ class Webfinger {
$params['resource'] = array(
'required' => true,
'type' => 'string',
'pattern' => '^(acct:)|^(https?://)(.+)$',
'type' => 'string',
'pattern' => '^(acct:)|^(https?://)(.+)$',
);
return $params;
@ -99,47 +99,17 @@ class Webfinger {
/**
* Get the WebFinger profile.
*
* @param string $resource the WebFinger resource.
* @param string $webfinger the WebFinger resource.
*
* @return array the WebFinger profile.
* @return array|\WP_Error The WebFinger profile or WP_Error if not found.
*/
public static function get_profile( $resource ) {
$user = User_Collection::get_by_resource( $resource );
if ( \is_wp_error( $user ) ) {
return $user;
}
$aliases = array(
$user->get_url(),
$user->get_alternate_url(),
);
$aliases = array_unique( $aliases );
$profile = array(
'subject' => sprintf( 'acct:%s', $user->get_webfinger() ),
'aliases' => array_values( array_unique( $aliases ) ),
'links' => array(
array(
'rel' => 'self',
'type' => 'application/activity+json',
'href' => $user->get_url(),
),
array(
'rel' => 'http://webfinger.net/rel/profile-page',
'type' => 'text/html',
'href' => $user->get_url(),
),
),
);
if ( 'Person' !== $user->get_type() ) {
$profile['links'][0]['properties'] = array(
'https://www.w3.org/ns/activitystreams#type' => $user->get_type(),
);
}
return $profile;
public static function get_profile( $webfinger ) {
/**
* Filter the WebFinger data.
*
* @param array $data The WebFinger data.
* @param string $webfinger The WebFinger resource.
*/
return apply_filters( 'webfinger_data', array(), $webfinger );
}
}