Merge branch 'master' into add/follow-me-block
Some checks failed
PHP_CodeSniffer / phpcs (push) Failing after 2s
Unit Testing / phpunit (5.6, 6.2) (push) Failing after 2s
Unit Testing / phpunit (7.0) (push) Failing after 3s
Unit Testing / phpunit (7.2) (push) Failing after 2s
Unit Testing / phpunit (7.3) (push) Failing after 2s
Unit Testing / phpunit (7.4) (push) Failing after 3s
Unit Testing / phpunit (8.0) (push) Failing after 2s
Unit Testing / phpunit (8.1) (push) Failing after 2s
Unit Testing / phpunit (8.2) (push) Failing after 2s
Unit Testing / phpunit (latest) (push) Failing after 2s

This commit is contained in:
Matt Wiebe 2023-08-11 17:42:46 -05:00
commit a5c25a1e5d
10 changed files with 233 additions and 50 deletions

View file

@ -75,11 +75,10 @@ Implemented:
* follow (accept follows) * follow (accept follows)
* share posts * share posts
* receive comments/reactions * receive comments/reactions
* signature verification
To implement: To implement:
* signature verification
* better WordPress integration
* better configuration possibilities * better configuration possibilities
* threaded comments support * threaded comments support
@ -120,6 +119,7 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu
* Add: Signature Verification: https://docs.joinmastodon.org/spec/security/ . * Add: Signature Verification: https://docs.joinmastodon.org/spec/security/ .
* Add: a Followers Block. * Add: a Followers Block.
* Add: Simple caching * Add: Simple caching
* Add: Collection endpoints for Featured Tags and Featured Posts
* Update: Complete rewrite of the Follower-System based on Custom Post Types. * Update: Complete rewrite of the Follower-System based on Custom Post Types.
* Update: Improved linter (PHPCS) * Update: Improved linter (PHPCS)
* Compatibility: Add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. * Compatibility: Add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests.
@ -127,6 +127,8 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu
* Compatibility: Indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.3. * Compatibility: Indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.3.
* Compatibility: Avoid PHP notice on sites using PHP 8.2. * Compatibility: Avoid PHP notice on sites using PHP 8.2.
* Fixed: Load the plugin later in the WordPress code lifecycle to avoid errors in some requests. * Fixed: Load the plugin later in the WordPress code lifecycle to avoid errors in some requests.
* Fixed: Updating posts
* Fixed: Hashtag now support CamelCase and UTF-8
### 0.17.0 ### ### 0.17.0 ###

View file

@ -52,7 +52,7 @@ class Http {
$response = \wp_safe_remote_post( $url, $args ); $response = \wp_safe_remote_post( $url, $args );
$code = \wp_remote_retrieve_response_code( $response ); $code = \wp_remote_retrieve_response_code( $response );
if ( 400 <= $code && 500 >= $code ) { if ( $code >= 400 ) {
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) );
} }
@ -100,7 +100,7 @@ class Http {
$response = \wp_safe_remote_get( $url, $args ); $response = \wp_safe_remote_get( $url, $args );
$code = \wp_remote_retrieve_response_code( $response ); $code = \wp_remote_retrieve_response_code( $response );
if ( 400 <= $code && 500 >= $code ) { if ( $code >= 400 ) {
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) );
} }

View file

@ -59,8 +59,7 @@ class Mention {
); );
$the_content = \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( self::class, 'replace_with_links' ), $the_content ); $the_content = \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( self::class, 'replace_with_links' ), $the_content );
$the_content = \str_replace( array_reverse( array_keys( $protected_tags ) ), array_reverse( array_values( $protected_tags ) ), $the_content );
$the_content = str_replace( array_reverse( array_keys( $protected_tags ) ), array_reverse( array_values( $protected_tags ) ), $the_content );
return $the_content; return $the_content;
} }
@ -74,6 +73,7 @@ class Mention {
*/ */
public static function replace_with_links( $result ) { public static function replace_with_links( $result ) {
$metadata = get_remote_metadata_by_actor( $result[0] ); $metadata = get_remote_metadata_by_actor( $result[0] );
if ( ! is_wp_error( $metadata ) && ! empty( $metadata['url'] ) ) { if ( ! is_wp_error( $metadata ) && ! empty( $metadata['url'] ) ) {
$username = ltrim( $result[0], '@' ); $username = ltrim( $result[0], '@' );
if ( ! empty( $metadata['name'] ) ) { if ( ! empty( $metadata['name'] ) ) {

View file

@ -62,7 +62,7 @@ class Webfinger {
$response = \wp_remote_get( $response = \wp_remote_get(
$url, $url,
array( array(
'headers' => array( 'Accept' => 'application/activity+json' ), 'headers' => array( 'Accept' => 'application/jrd+json' ),
'redirection' => 0, 'redirection' => 0,
'timeout' => 2, 'timeout' => 2,
) )
@ -94,4 +94,110 @@ class Webfinger {
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period. \set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
return $link; return $link;
} }
/**
* 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 ) ) {
return new WP_Error( 'invalid_identifier', __( 'Invalid Identifier', 'activitypub' ) );
}
return array( $identifier, $host );
}
/**
* Get the WebFinger data for a given URI
*
* @param string $identifier The Identifier: <identifier>@<host>
* @param string $host The Host: <identifier>@<host>
*
* @return WP_Error|array Error reaction or array with
* identifier and host as values
*/
public static function get_data( $identifier, $host ) {
$webfinger_url = 'https://' . $host . '/.well-known/webfinger?resource=' . rawurlencode( $identifier );
$response = wp_safe_remote_get(
$webfinger_url,
array(
'headers' => array( 'Accept' => 'application/jrd+json' ),
'redirection' => 0,
'timeout' => 2,
)
);
if ( is_wp_error( $response ) ) {
return new WP_Error( 'webfinger_url_not_accessible', null, $webfinger_url );
}
$body = wp_remote_retrieve_body( $response );
return json_decode( $body, true );
}
/**
* Undocumented function
*
* @return void
*/
public static function get_remote_follow_endpoint( $uri ) {
$identifier_and_host = self::get_identifier_and_host( $uri );
if ( is_wp_error( $identifier_and_host ) ) {
return $identifier_and_host;
}
list( $identifier, $host ) = $identifier_and_host;
$data = self::get_data( $identifier, $host );
if ( is_wp_error( $data ) ) {
return $data;
}
if ( empty( $data['links'] ) ) {
return new WP_Error( 'webfinger_url_invalid_response', null, $data );
}
foreach ( $data['links'] as $link ) {
if ( 'http://ostatus.org/schema/1.0/subscribe' === $link['rel'] ) {
return $link['template'];
}
}
return new WP_Error( 'webfinger_remote_follow_endpoint_invalid', null, $data );
}
} }

View file

@ -265,11 +265,18 @@ class Followers {
return; return;
} }
if ( isset( $object['user_id'] ) ) { // only send minimal data
unset( $object['user_id'] ); $object = array_intersect_key(
} $object,
array_flip(
unset( $object['@context'] ); array(
'id',
'type',
'actor',
'object',
)
)
);
$user = Users::get_by_id( $user_id ); $user = Users::get_by_id( $user_id );
@ -282,7 +289,7 @@ class Followers {
$activity->set_object( $object ); $activity->set_object( $object );
$activity->set_actor( $user->get_id() ); $activity->set_actor( $user->get_id() );
$activity->set_to( $actor ); $activity->set_to( $actor );
$activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) ); $activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) . '-' . \time() );
$activity = $activity->to_json(); $activity = $activity->to_json();

View file

@ -305,8 +305,9 @@ function is_activitypub_request() {
* and return true when the header includes at least one of the following: * and return true when the header includes at least one of the following:
* - application/activity+json * - application/activity+json
* - application/ld+json * - application/ld+json
* - application/json
*/ */
if ( preg_match( '/(application\/(ld\+json|activity\+json))/', $accept ) ) { if ( preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) {
return true; return true;
} }
} }

View file

@ -4,8 +4,11 @@ namespace Activitypub\Rest;
use WP_REST_Server; use WP_REST_Server;
use WP_REST_Response; use WP_REST_Response;
use Activitypub\Transformer\Post; use Activitypub\Transformer\Post;
use Activitypub\Activity\Activity;
use Activitypub\Collection\Users as User_Collection;
use function Activitypub\esc_hashtag; use function Activitypub\esc_hashtag;
use function Activitypub\is_single_user;
use function Activitypub\get_rest_url_by_path; use function Activitypub\get_rest_url_by_path;
/** /**
@ -64,6 +67,12 @@ class Collection {
*/ */
public static function tags_get( $request ) { public static function tags_get( $request ) {
$user_id = $request->get_param( 'user_id' ); $user_id = $request->get_param( 'user_id' );
$user = User_Collection::get_by_various( $user_id );
if ( is_wp_error( $user ) ) {
return $user;
}
$number = 4; $number = 4;
$tags = \get_terms( $tags = \get_terms(
@ -80,13 +89,9 @@ class Collection {
} }
$response = array( $response = array(
'@context' => array( '@context' => Activity::CONTEXT,
'https://www.w3.org/ns/activitystreams', 'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/tags', $user->get__id() ) ),
array( 'type' => 'Collection',
'Hashtag' => 'as:Hashtag',
),
),
'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/tags', $user_id ) ),
'totalItems' => count( $tags ), 'totalItems' => count( $tags ),
'items' => array(), 'items' => array(),
); );
@ -111,36 +116,43 @@ class Collection {
*/ */
public static function featured_get( $request ) { public static function featured_get( $request ) {
$user_id = $request->get_param( 'user_id' ); $user_id = $request->get_param( 'user_id' );
$user = User_Collection::get_by_various( $user_id );
if ( is_wp_error( $user ) ) {
return $user;
}
$sticky_posts = \get_option( 'sticky_posts' );
if ( ! is_single_user() && User_Collection::BLOG_USER_ID === $user->get__id() ) {
$posts = array();
} elseif ( $sticky_posts ) {
$args = array( $args = array(
'post__in' => \get_option( 'sticky_posts' ), 'post__in' => $sticky_posts,
'ignore_sticky_posts' => 1, 'ignore_sticky_posts' => 1,
'orderby' => 'date',
'order' => 'DESC',
); );
if ( $user_id > 0 ) { if ( $user->get__id() > 0 ) {
$args['author'] = $user_id; $args['author'] = $user->get__id();
} }
$posts = \get_posts( $args ); $posts = \get_posts( $args );
} else {
$posts = array();
}
$response = array( $response = array(
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => Activity::CONTEXT,
array(
'ostatus' => 'http://ostatus.org#',
'atomUri' => 'ostatus:atomUri',
'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri',
'conversation' => 'ostatus:conversation',
'sensitive' => 'as:sensitive',
'toot' => 'http://joinmastodon.org/ns#',
'votersCount' => 'toot:votersCount',
),
'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $user_id ) ), 'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $user_id ) ),
'type' => 'OrderedCollection',
'totalItems' => count( $posts ), 'totalItems' => count( $posts ),
'items' => array(), 'orderedItems' => array(),
); );
foreach ( $posts as $post ) { foreach ( $posts as $post ) {
$response['items'][] = Post::transform( $post )->to_object()->to_array(); $response['orderedItems'][] = Post::transform( $post )->to_object()->to_array();
} }
return new WP_REST_Response( $response, 200 ); return new WP_REST_Response( $response, 200 );

View file

@ -3,7 +3,9 @@ namespace Activitypub\Rest;
use WP_Error; use WP_Error;
use WP_REST_Server; use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
use Activitypub\Webfinger;
use Activitypub\Activity\Activity; use Activitypub\Activity\Activity;
use Activitypub\Collection\Users as User_Collection; use Activitypub\Collection\Users as User_Collection;
@ -40,6 +42,25 @@ class Users {
), ),
) )
); );
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/users/(?P<user_id>[\w\-\.]+)/remote-follow',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( self::class, 'remote_follow_get' ),
'args' => array(
'resource' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
),
'permission_callback' => '__return_true',
),
)
);
} }
/** /**
@ -80,6 +101,38 @@ class Users {
return $response; return $response;
} }
/**
* Endpoint for remote follow UI/Block
*
* @param WP_REST_Request $request The request object.
*
* @return void|string The URL to the remote follow page
*/
public static function remote_follow_get( WP_REST_Request $request ) {
$resource = $request->get_param( 'resource' );
$user_id = $request->get_param( 'user_id' );
$user = User_Collection::get_by_various( $user_id );
if ( is_wp_error( $user ) ) {
return $user;
}
$template = Webfinger::get_remote_follow_endpoint( $resource );
if ( is_wp_error( $template ) ) {
return $template;
}
$resource = $user->get_resource();
$url = str_replace( '{uri}', $resource, $template );
return new WP_REST_Response(
array( 'url' => $url ),
200
);
}
/** /**
* The supported parameters * The supported parameters
* *

View file

@ -75,11 +75,10 @@ Implemented:
* follow (accept follows) * follow (accept follows)
* share posts * share posts
* receive comments/reactions * receive comments/reactions
* signature verification
To implement: To implement:
* signature verification
* better WordPress integration
* better configuration possibilities * better configuration possibilities
* threaded comments support * threaded comments support
@ -120,6 +119,7 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu
* Add: Signature Verification: https://docs.joinmastodon.org/spec/security/ . * Add: Signature Verification: https://docs.joinmastodon.org/spec/security/ .
* Add: a Followers Block. * Add: a Followers Block.
* Add: Simple caching * Add: Simple caching
* Add: Collection endpoints for Featured Tags and Featured Posts
* Update: Complete rewrite of the Follower-System based on Custom Post Types. * Update: Complete rewrite of the Follower-System based on Custom Post Types.
* Update: Improved linter (PHPCS) * Update: Improved linter (PHPCS)
* Compatibility: Add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. * Compatibility: Add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests.
@ -127,6 +127,8 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu
* Compatibility: Indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.3. * Compatibility: Indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.3.
* Compatibility: Avoid PHP notice on sites using PHP 8.2. * Compatibility: Avoid PHP notice on sites using PHP 8.2.
* Fixed: Load the plugin later in the WordPress code lifecycle to avoid errors in some requests. * Fixed: Load the plugin later in the WordPress code lifecycle to avoid errors in some requests.
* Fixed: Updating posts
* Fixed: Hashtag now support CamelCase and UTF-8
= 0.17.0 = = 0.17.0 =

View file

@ -30,13 +30,13 @@
<label for="activitypub-blog-identifier"><?php \esc_html_e( 'Username', 'activitypub' ); ?></label> <label for="activitypub-blog-identifier"><?php \esc_html_e( 'Username', 'activitypub' ); ?></label>
</p> </p>
<p> <p>
<input type="text" class="regular-text" id="activitypub-blog-identifier" value="<?php echo \esc_attr( $blog_user->get_resource() ); ?>" /> <input type="text" class="regular-text" id="activitypub-blog-identifier" value="<?php echo \esc_attr( $blog_user->get_resource() ); ?>" readonly />
</p> </p>
<p> <p>
<label for="activitypub-blog-url"><?php \esc_html_e( 'Profile-URL', 'activitypub' ); ?></label> <label for="activitypub-blog-url"><?php \esc_html_e( 'Profile-URL', 'activitypub' ); ?></label>
</p> </p>
<p> <p>
<input type="text" class="regular-text" id="activitypub-blog-url" value="<?php echo \esc_attr( $blog_user->get_url() ); ?>" /> <input type="text" class="regular-text" id="activitypub-blog-url" value="<?php echo \esc_attr( $blog_user->get_url() ); ?>" readonly />
</p> </p>
<p> <p>
<?php \esc_html_e( 'This Blog-User will federate all posts written on your Blog, regardless of the User who posted it.', 'activitypub' ); ?> <?php \esc_html_e( 'This Blog-User will federate all posts written on your Blog, regardless of the User who posted it.', 'activitypub' ); ?>
@ -62,13 +62,13 @@
<label for="activitypub-user-identifier"><?php \esc_html_e( 'Username', 'activitypub' ); ?></label> <label for="activitypub-user-identifier"><?php \esc_html_e( 'Username', 'activitypub' ); ?></label>
</p> </p>
<p> <p>
<input type="text" class="regular-text" id="activitypub-user-identifier" value="<?php echo \esc_attr( $user->get_resource() ); ?>" /> <input type="text" class="regular-text" id="activitypub-user-identifier" value="<?php echo \esc_attr( $user->get_resource() ); ?>" readonly />
</p> </p>
<p> <p>
<label for="activitypub-user-url"><?php \esc_html_e( 'Profile-URL', 'activitypub' ); ?></label> <label for="activitypub-user-url"><?php \esc_html_e( 'Profile-URL', 'activitypub' ); ?></label>
</p> </p>
<p> <p>
<input type="text" class="regular-text" id="activitypub-user-url" value="<?php echo \esc_attr( $user->get_url() ); ?>" /> <input type="text" class="regular-text" id="activitypub-user-url" value="<?php echo \esc_attr( $user->get_url() ); ?>" readonly />
</p> </p>
<p> <p>
<?php \esc_html_e( 'Users who can not access this settings page will find their username on the "Edit Profile" page.', 'activitypub' ); ?> <?php \esc_html_e( 'Users who can not access this settings page will find their username on the "Edit Profile" page.', 'activitypub' ); ?>