Merge commit 'a8078ce72bc93a7d095f19b3bdde055e68939fe8' into add/transformer-management

This commit is contained in:
André Menrath 2023-12-22 19:08:50 +01:00
commit ea844ee942
15 changed files with 290 additions and 68 deletions

View file

@ -114,6 +114,7 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu
* Added: CSS class for ActivityPub comments to allow custom designs
* Added: FEP-2677: Identifying the Application Actor
* Added: Basic Comment Federation
* Added: Profile Update Activities
* Improved: WebFinger endpoints
### 1.3.0 ###

View file

@ -33,6 +33,7 @@ require_once __DIR__ . '/includes/functions.php';
\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "<strong>[ap_title]</strong>\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" );
\defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false );
\defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false );
\defined( 'ACTIVITYPUB_SHARED_INBOX_FEATURE' ) || \define( 'ACTIVITYPUB_SHARED_INBOX_FEATURE', false );
\define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
\define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );

View file

@ -28,6 +28,7 @@ class Activity_Dispatcher {
public static function init() {
\add_action( 'activitypub_send_activity', array( self::class, 'send_activity' ), 10, 2 );
\add_action( 'activitypub_send_activity', array( self::class, 'send_activity_or_announce' ), 10, 2 );
\add_action( 'activitypub_send_update_profile_activity', array( self::class, 'send_profile_update' ), 10, 1 );
}
/**
@ -74,19 +75,9 @@ class Activity_Dispatcher {
return;
}
$activity = $transformer->to_activity( 'Create' );
$activity = $transformer->to_activity( $type );
$follower_inboxes = Followers::get_inboxes( $user_id );
$mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() );
$inboxes = array_merge( $follower_inboxes, $mentioned_inboxes );
$inboxes = array_unique( $inboxes );
$json = $activity->to_json();
foreach ( $inboxes as $inbox ) {
safe_remote_post( $inbox, $json, $user_id );
}
self::send_activity_to_inboxes( $activity, $user_id );
}
/**
@ -112,12 +103,60 @@ class Activity_Dispatcher {
$user_id = $transformer->get_wp_user_id();
$activity = $transformer->to_activity( 'Announce' );
$follower_inboxes = Followers::get_inboxes( $user_id );
$mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() );
self::send_activity_to_inboxes( $activity, $user_id );
}
/**
* Send a "Update" Activity when a user updates their profile.
*
* @param int $user_id The user ID to send an update for.
*
* @return void
*/
public static function send_profile_update( $user_id ) {
$user = Users::get_by_various( $user_id );
// bail if that's not a good user
if ( is_wp_error( $user ) ) {
return;
}
// build the update
$activity = new Activity();
$activity->set_id( $user->get_url() . '#update' );
$activity->set_type( 'Update' );
$activity->set_actor( $user->get_url() );
$activity->set_object( $user->get_url() );
$activity->set_to( 'https://www.w3.org/ns/activitystreams#Public' );
// send the update
self::send_activity_to_inboxes( $activity, $user_id );
}
/**
* Send an Activity to all followers and mentioned users.
*
* @param Activity $activity The ActivityPub Activity.
* @param int $user_id The user ID.
*
* @return void
*/
private static function send_activity_to_inboxes( $activity, $user_id ) {
$follower_inboxes = Followers::get_inboxes( $user_id );
$mentioned_inboxes = array();
$cc = $activity->get_cc();
if ( $cc ) {
$mentioned_inboxes = Mention::get_inboxes( $cc );
}
$inboxes = array_merge( $follower_inboxes, $mentioned_inboxes );
$inboxes = array_unique( $inboxes );
if ( empty( $inboxes ) ) {
return;
}
$json = $activity->to_json();
foreach ( $inboxes as $inbox ) {

View file

@ -18,6 +18,7 @@ class Admin {
\add_action( 'admin_init', array( self::class, 'register_settings' ) );
\add_action( 'personal_options_update', array( self::class, 'save_user_description' ) );
\add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
\add_action( 'admin_notices', array( self::class, 'admin_notices' ) );
if ( ! is_user_disabled( get_current_user_id() ) ) {
\add_action( 'show_user_profile', array( self::class, 'add_profile' ) );
@ -46,6 +47,37 @@ class Admin {
}
}
/**
* Display admin menu notices about configuration problems or conflicts.
*
* @return void
*/
public static function admin_notices() {
$permalink_structure = \get_option( 'permalink_structure' );
if ( empty( $permalink_structure ) ) {
$admin_notice = \__( 'You are using the ActivityPub plugin without setting a permalink structure. This will prevent ActivityPub from working. Please set a permalink structure.', 'activitypub' );
self::show_admin_notice( $admin_notice, 'error' );
}
}
/**
* Display one admin menu notice about configuration problems or conflicts.
*
* @param string $admin_notice The notice to display.
* @param string $level The level of the notice (error, warning, success, info).
*
* @return void
*/
private static function show_admin_notice( $admin_notice, $level ) {
?>
<div class="notice notice-<?php echo esc_attr( $level ); ?>">
<p><?php echo wp_kses( $admin_notice, 'data' ); ?></p>
</div>
<?php
}
/**
* Load settings page
*/

View file

@ -6,12 +6,15 @@ use Activitypub\Collection\Users;
use Activitypub\Collection\Followers;
use Activitypub\Transformer\Post;
use function Activitypub\is_user_type_disabled;
/**
* ActivityPub Scheduler Class
*
* @author Matthias Pfefferle
*/
class Scheduler {
/**
* Initialize the class, registering WordPress hooks
*/
@ -58,6 +61,22 @@ class Scheduler {
// Migration
\add_action( 'admin_init', array( self::class, 'schedule_migration' ) );
// profile updates for blog options
if ( ! is_user_type_disabled( 'blog' ) ) {
\add_action( 'update_option_site_icon', array( self::class, 'blog_user_update' ) );
\add_action( 'update_option_blogdescription', array( self::class, 'blog_user_update' ) );
\add_action( 'update_option_blogname', array( self::class, 'blog_user_update' ) );
\add_filter( 'pre_set_theme_mod_custom_logo', array( self::class, 'blog_user_update' ) );
\add_filter( 'pre_set_theme_mod_header_image', array( self::class, 'blog_user_update' ) );
}
// profile updates for user options
if ( ! is_user_type_disabled( 'user' ) ) {
\add_action( 'wp_update_user', array( self::class, 'user_update' ) );
\add_action( 'updated_user_meta', array( self::class, 'user_meta_update' ), 10, 3 );
// @todo figure out a feasible way of updating the header image since it's not unique to any user.
}
}
/**
@ -255,4 +274,68 @@ class Scheduler {
\wp_schedule_single_event( \time(), 'activitypub_schedule_migration' );
}
}
/**
* Send a profile update when relevant user meta is updated.
*
* @param int $meta_id Meta ID being updated.
* @param int $user_id User ID being updated.
* @param string $meta_key Meta key being updated.
*
* @return void
*/
public static function user_meta_update( $meta_id, $user_id, $meta_key ) {
// don't bother if the user can't publish
if ( ! \user_can( $user_id, 'publish_posts' ) ) {
return;
}
// the user meta fields that affect a profile.
$fields = array(
'activitypub_user_description',
'description',
'user_url',
'display_name',
);
if ( in_array( $meta_key, $fields, true ) ) {
self::schedule_profile_update( $user_id );
}
}
/**
* Send a profile update when a user is updated.
*
* @param int $user_id User ID being updated.
*
* @return void
*/
public static function user_update( $user_id ) {
// don't bother if the user can't publish
if ( ! \user_can( $user_id, 'publish_posts' ) ) {
return;
}
self::schedule_profile_update( $user_id );
}
/**
* Theme mods only have a dynamic filter so we fudge it like this.
* @param mixed $value
* @return mixed
*/
public static function blog_user_update( $value = null ) {
self::schedule_profile_update( 0 );
return $value;
}
/**
* Send a profile update to all followers. Gets hooked into all relevant options/meta etc.
* @param int $user_id The user ID to update (Could be 0 for Blog-User).
*/
public static function schedule_profile_update( $user_id ) {
\wp_schedule_single_event(
\time(),
'activitypub_send_update_profile_activity',
array( $user_id )
);
}
}

View file

@ -15,7 +15,12 @@ class Create {
* Initialize the class, registering WordPress hooks
*/
public static function init() {
\add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 3 );
\add_action(
'activitypub_inbox_create',
array( self::class, 'handle_create' ),
10,
3
);
}
/**

View file

@ -15,11 +15,24 @@ class Delete {
* Initialize the class, registering WordPress hooks
*/
public static function init() {
\add_action( 'activitypub_inbox_delete', array( self::class, 'handle_delete' ), 10, 2 );
\add_action(
'activitypub_inbox_delete',
array( self::class, 'handle_delete' )
);
// defer signature verification for `Delete` requests.
\add_filter( 'activitypub_defer_signature_verification', array( self::class, 'defer_signature_verification' ), 10, 2 );
\add_filter(
'activitypub_defer_signature_verification',
array( self::class, 'defer_signature_verification' ),
10,
2
);
// side effect
\add_action( 'activitypub_delete_actor_interactions', array( self::class, 'delete_interactions' ), 10, 1 );
\add_action(
'activitypub_delete_actor_interactions',
array( self::class, 'delete_interactions' )
);
}
/**
@ -28,7 +41,7 @@ class Delete {
* @param array $activity The delete activity.
* @param int $user_id The ID of the user performing the delete activity.
*/
public static function handle_delete( $activity, $user_id ) {
public static function handle_delete( $activity ) {
$object_type = isset( $activity['object']['type'] ) ? $activity['object']['type'] : '';
switch ( $object_type ) {
@ -39,7 +52,7 @@ class Delete {
case 'Organization':
case 'Service':
case 'Application':
self::maybe_delete_follower( $user_id, $activity );
self::maybe_delete_follower( $activity );
break;
// Object and Link Types
// @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types

View file

@ -14,8 +14,17 @@ class Follow {
* Initialize the class, registering WordPress hooks
*/
public static function init() {
\add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow' ), 10, 2 );
\add_action( 'activitypub_followers_post_follow', array( self::class, 'send_follow_response' ), 10, 4 );
\add_action(
'activitypub_inbox_follow',
array( self::class, 'handle_follow' )
);
\add_action(
'activitypub_followers_post_follow',
array( self::class, 'send_follow_response' ),
10,
4
);
}
/**
@ -24,11 +33,30 @@ class Follow {
* @param array $activity The activity object
* @param int $user_id The user ID
*/
public static function handle_follow( $activity, $user_id ) {
// save follower
$follower = Followers::add_follower( $user_id, $activity['actor'] );
public static function handle_follow( $activity ) {
$user = Users::get_by_resource( $activity['object'] );
do_action( 'activitypub_followers_post_follow', $activity['actor'], $activity, $user_id, $follower );
if ( ! $user || is_wp_error( $user ) ) {
// If we can not find a user,
// we can not initiate a follow process
return;
}
$user_id = $user->get__id();
// save follower
$follower = Followers::add_follower(
$user_id,
$activity['actor']
);
do_action(
'activitypub_followers_post_follow',
$activity['actor'],
$activity,
$user_id,
$follower
);
}
/**

View file

@ -1,6 +1,7 @@
<?php
namespace Activitypub\Handler;
use Activitypub\Collection\Users;
use Activitypub\Collection\Followers;
/**
@ -11,7 +12,10 @@ class Undo {
* Initialize the class, registering WordPress hooks
*/
public static function init() {
\add_action( 'activitypub_inbox_undo', array( self::class, 'handle_undo' ), 10, 2 );
\add_action(
'activitypub_inbox_undo',
array( self::class, 'handle_undo' )
);
}
/**
@ -20,11 +24,23 @@ class Undo {
* @param array $activity The JSON "Undo" Activity
* @param int $user_id The ID of the ID of the WordPress User
*/
public static function handle_undo( $activity, $user_id ) {
public static function handle_undo( $activity ) {
if (
isset( $activity['object']['type'] ) &&
'Follow' === $activity['object']['type']
'Follow' === $activity['object']['type'] &&
isset( $activity['object']['object'] ) &&
filter_var( $activity['object']['object'], FILTER_VALIDATE_URL )
) {
$user = Users::get_by_resource( $activity['object']['object'] );
if ( ! $user || is_wp_error( $user ) ) {
// If we can not find a user,
// we can not initiate a follow process
return;
}
$user_id = $user->get__id();
Followers::remove_follower( $user_id, $activity['actor'] );
}
}

View file

@ -14,7 +14,10 @@ class Update {
* Initialize the class, registering WordPress hooks
*/
public static function init() {
\add_action( 'activitypub_inbox_update', array( self::class, 'handle_update' ), 10, 2 );
\add_action(
'activitypub_inbox_update',
array( self::class, 'handle_update' )
);
}
/**
@ -23,7 +26,7 @@ class Update {
* @param array $array The activity-object
* @param int $user_id The id of the local blog-user
*/
public static function handle_update( $array, $user_id ) {
public static function handle_update( $array ) {
$object_type = isset( $array['object']['type'] ) ? $array['object']['type'] : '';
switch ( $object_type ) {
@ -45,7 +48,7 @@ class Update {
case 'Video':
case 'Event':
case 'Document':
self::update_interaction( $array, $user_id );
self::update_interaction( $array );
break;
// Minimal Activity
// @see https://www.w3.org/TR/activitystreams-core/#example-1
@ -62,7 +65,7 @@ class Update {
*
* @return void
*/
public static function update_interaction( $activity, $user_id ) {
public static function update_interaction( $activity ) {
$state = Interactions::update_comment( $activity );
$reaction = null;
@ -70,7 +73,7 @@ class Update {
$reaction = \get_comment( $state );
}
\do_action( 'activitypub_handled_update', $activity, $user_id, $state, $reaction );
\do_action( 'activitypub_handled_update', $activity, null, $state, $reaction );
}
/**

View file

@ -226,6 +226,18 @@ class User extends Actor {
return get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $this->get__id() ) );
}
public function get_endpoints() {
$endpoints = null;
if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) {
$endpoints = array(
'sharedInbox' => get_rest_url_by_path( 'inbox' ),
);
}
return $endpoints;
}
/**
* Extend the User-Output with Attachments.
*

View file

@ -94,11 +94,8 @@ class Inbox {
$json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' );
$json->type = 'OrderedCollectionPage';
$json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get__id() ) ); // phpcs:ignore
$json->totalItems = 0; // phpcs:ignore
$json->orderedItems = array(); // phpcs:ignore
$json->first = $json->partOf; // phpcs:ignore
// filter output
@ -155,37 +152,10 @@ class Inbox {
$data = $request->get_json_params();
$activity = Activity::init_from_array( $data );
$type = $request->get_param( 'type' );
$users = self::get_recipients( $data );
$type = \strtolower( $type );
if ( ! $users ) {
return new WP_Error(
'rest_invalid_param',
\__( 'No recipients found', 'activitypub' ),
array(
'status' => 400,
'params' => array(
'to' => \__( 'Please check/validate "to" field', 'activitypub' ),
'bto' => \__( 'Please check/validate "bto" field', 'activitypub' ),
'cc' => \__( 'Please check/validate "cc" field', 'activitypub' ),
'bcc' => \__( 'Please check/validate "bcc" field', 'activitypub' ),
'audience' => \__( 'Please check/validate "audience" field', 'activitypub' ),
),
)
);
}
foreach ( $users as $user ) {
$user = User_Collection::get_by_various( $user );
if ( is_wp_error( $user ) ) {
continue;
}
$type = \strtolower( $type );
\do_action( 'activitypub_inbox', $data, $user->ID, $type, $activity );
\do_action( "activitypub_inbox_{$type}", $data, $user->ID, $activity );
}
\do_action( 'activitypub_inbox', $data, null, $type, $activity );
\do_action( "activitypub_inbox_{$type}", $data, null, $activity );
$rest_response = new WP_REST_Response( array(), 202 );
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );

View file

@ -395,7 +395,7 @@ class Post extends Base {
*
* @return string The Object-Type.
*/
protected function get_object_type() {
protected function get_type() {
if ( 'wordpress-post-format' !== \get_option( 'activitypub_object_type', 'note' ) ) {
return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) );
}

View file

@ -114,6 +114,7 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu
* Added: CSS class for ActivityPub comments to allow custom designs
* Added: FEP-2677: Identifying the Application Actor
* Added: Basic Comment Federation
* Added: Profile Update Activities
* Improved: WebFinger endpoints
= 1.3.0 =

View file

@ -0,0 +1,18 @@
<?php
class Test_Admin extends WP_UnitTestCase {
public function test_no_permalink_structure_has_errors() {
\add_option( 'permalink_structure', '' );
\do_action( 'admin_notices' );
$this->expectOutputRegex( "/notice-error/" );
\delete_option( 'permalink_structure' );
}
public function test_has_permalink_structure_no_errors() {
\add_option( 'permalink_structure', '/archives/%post_id%' );
\do_action( 'admin_notices' );
$this->expectOutputRegex( "/^((?!notice-error).)*$/s" );
\delete_option( 'permalink_structure' );
}
}