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

@ -0,0 +1,95 @@
<?php
/**
* Outbox Add handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler\Outbox;
use Activitypub\Collection\Actors;
use function Activitypub\object_to_uri;
/**
* Handle outgoing Add activities.
*
* Supports adding objects to an actor's featured collection
* by making the corresponding WordPress post sticky.
*/
class Add {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'activitypub_outbox_add', array( self::class, 'handle_add' ), 10, 2 );
}
/**
* Handle outgoing "Add" activities from local actors.
*
* When the target is the actor's featured collection, the referenced
* post is made sticky. The sticky action triggers the scheduler which
* creates the outbox entry automatically.
*
* @since 8.1.0
*
* @param array $data The activity data array.
* @param int $user_id The user ID.
*
* @return \WP_Post|\WP_Error|array The post object on success, WP_Error on failure, or original data if unhandled.
*/
public static function handle_add( $data, $user_id = null ) {
$object_uri = object_to_uri( $data['object'] ?? '' );
$target = object_to_uri( $data['target'] ?? '' );
if ( empty( $object_uri ) || empty( $target ) ) {
return $data;
}
$actor = Actors::get_by_id( $user_id );
if ( \is_wp_error( $actor ) ) {
return $actor;
}
// Only handle featured collection targets.
if ( $target !== $actor->get_featured() ) {
return $data;
}
$post_id = \url_to_postid( $object_uri );
if ( ! $post_id ) {
return new \WP_Error(
'activitypub_object_not_found',
\__( 'The referenced object was not found.', 'activitypub' ),
array( 'status' => 404 )
);
}
$post = \get_post( $post_id );
if ( ! $post ) {
return new \WP_Error(
'activitypub_object_not_found',
\__( 'The referenced object was not found.', 'activitypub' ),
array( 'status' => 404 )
);
}
// Verify the user owns this post.
if ( $user_id > 0 && (int) $post->post_author !== $user_id ) {
return new \WP_Error(
'activitypub_forbidden',
\__( 'You can only feature your own posts.', 'activitypub' ),
array( 'status' => 403 )
);
}
// Making the post sticky triggers the scheduler which adds to outbox.
\stick_post( $post_id );
return $post;
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* Outbox Announce handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler\Outbox;
use function Activitypub\object_to_uri;
/**
* Handle outgoing Announce activities.
*/
class Announce {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'activitypub_outbox_announce', array( self::class, 'handle_announce' ), 10, 2 );
}
/**
* Handle outgoing "Announce" activities from local actors.
*
* Records an announce/boost from the local user on remote content.
*
* @param array $data The activity data array.
* @param int $user_id The user ID.
*/
public static function handle_announce( $data, $user_id = null ) {
$object_url = object_to_uri( $data['object'] ?? '' );
if ( empty( $object_url ) ) {
return $data;
}
/**
* Fires after an outgoing Announce activity has been processed.
*
* @param string $object_url The URL of the announced object.
* @param array $data The activity data.
* @param int $user_id The user ID.
*/
\do_action( 'activitypub_outbox_announce_sent', $object_url, $data, $user_id );
return $data;
}
}

View File

@ -0,0 +1,187 @@
<?php
/**
* Outbox Arrive handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler\Outbox;
use Activitypub\Collection\Posts;
use function Activitypub\add_to_outbox;
use function Activitypub\is_activity_public;
/**
* Handle outgoing Arrive activities.
*
* @since 8.1.0
*/
class Arrive {
/**
* Initialize the class, registering WordPress hooks.
*
* @since 8.1.0
*/
public static function init() {
\add_filter( 'activitypub_outbox_arrive', array( self::class, 'handle_arrive' ), 10, 3 );
}
/**
* Handle outgoing "Arrive" activities from local actors.
*
* Arrive is an intransitive activity (no object) indicating that
* the actor has arrived at a location. Per the ActivityPub spec,
* the server must preserve the original activity type, so this
* handler adds the Arrive directly to the outbox as-is.
*
* As a local side effect, a WordPress post is created so the
* check-in appears on the blog with location geodata.
*
* @since 8.1.0
*
* @param array $data The activity data array.
* @param int $user_id The user ID.
* @param string|null $visibility Content visibility.
*
* @return int|\WP_Error|false The outbox post ID, error, or false.
*/
public static function handle_arrive( $data, $user_id = null, $visibility = null ) {
// Create a blog post for public check-ins so they appear on the site.
if ( is_activity_public( $data ) ) {
$post = self::create_checkin_post( $data, $user_id, $visibility );
if ( ! \is_wp_error( $post ) ) {
$data['url'] = \get_permalink( $post );
}
}
/*
* Add the original Arrive activity to the outbox directly.
* This preserves the intransitive activity type per the
* ActivityPub spec (Section 6) instead of wrapping it in Create.
*/
$outbox_id = add_to_outbox( $data, null, $user_id, $visibility );
if ( ! $outbox_id ) {
return new \WP_Error(
'activitypub_outbox_error',
\__( 'Failed to add Arrive activity to outbox.', 'activitypub' ),
array( 'status' => 500 )
);
}
return $outbox_id;
}
/**
* Create a WordPress post from the Arrive activity.
*
* Creates a blog post with the check-in content and saves
* location geodata so it can be displayed on the site.
*
* @since 8.1.0
*
* @param array $data The activity data.
* @param int $user_id The user ID.
* @param string|null $visibility Content visibility.
*
* @return \WP_Post|\WP_Error The created post or error.
*/
private static function create_checkin_post( $data, $user_id, $visibility ) {
$location = $data['location'] ?? null;
$location_name = self::get_location_name( $location );
$title = $location_name
? sprintf(
/* translators: %s: location name */
\__( 'Checked in at %s', 'activitypub' ),
$location_name
)
: \__( 'Check-in', 'activitypub' );
$activity = array(
'object' => array(
'type' => 'Note',
'name' => $title,
'content' => $data['content'] ?? $data['summary'] ?? '',
),
'to' => $data['to'] ?? array(),
'cc' => $data['cc'] ?? array(),
);
$post = Posts::create( $activity, $user_id, $visibility );
if ( \is_wp_error( $post ) ) {
return $post;
}
self::save_location( $post->ID, $location );
/**
* Fires after an Arrive activity has created a local blog post.
*
* @since 8.1.0
*
* @param int $post_id The created post ID.
* @param array|null $location The location data from the activity.
* @param array $data The activity data.
* @param int $user_id The user ID.
*/
\do_action( 'activitypub_outbox_arrive_sent', $post->ID, $location, $data, $user_id );
return $post;
}
/**
* Save location geodata on a post.
*
* Uses the standard `geo_*` meta keys that the Post transformer
* reads back when converting to ActivityPub Place objects.
*
* @since 8.1.0
*
* @param int $post_id The post ID.
* @param array|null $location The ActivityPub location data.
*/
private static function save_location( $post_id, $location ) {
if ( ! \is_array( $location ) ) {
return;
}
if ( ! empty( $location['name'] ) ) {
\update_post_meta( $post_id, 'geo_address', \sanitize_text_field( $location['name'] ) );
}
if ( isset( $location['latitude'] ) && \is_numeric( $location['latitude'] ) ) {
\update_post_meta( $post_id, 'geo_latitude', (float) $location['latitude'] );
}
if ( isset( $location['longitude'] ) && \is_numeric( $location['longitude'] ) ) {
\update_post_meta( $post_id, 'geo_longitude', (float) $location['longitude'] );
}
if ( ! empty( $location['name'] ) || ( isset( $location['latitude'] ) && isset( $location['longitude'] ) ) ) {
\update_post_meta( $post_id, 'geo_public', '1' );
}
}
/**
* Extract a human-readable name from an ActivityPub location.
*
* @param mixed $location The location data (array or string).
*
* @return string|null The location name or null.
*/
private static function get_location_name( $location ) {
if ( \is_array( $location ) && ! empty( $location['name'] ) ) {
return \sanitize_text_field( $location['name'] );
}
if ( \is_string( $location ) && ! empty( $location ) ) {
return \sanitize_text_field( $location );
}
return null;
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* Outbox Block handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler\Outbox;
use Activitypub\Moderation;
use function Activitypub\add_to_outbox;
use function Activitypub\object_to_uri;
/**
* Handle outgoing Block activities.
*/
class Block {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'activitypub_outbox_block', array( self::class, 'handle_block' ), 10, 2 );
}
/**
* Handle outgoing "Block" activities from local actors.
*
* Blocks a remote actor using the Moderation system, then adds
* the activity to the outbox for federation.
*
* @since 8.1.0
*
* @param array $data The activity data array.
* @param int $user_id The user ID.
*
* @return array|int|\WP_Error The original data if unhandled, outbox post ID on success, or WP_Error on failure.
*/
public static function handle_block( $data, $user_id = null ) {
$actor_uri = object_to_uri( $data['object'] ?? '' );
if ( empty( $actor_uri ) ) {
return $data;
}
$result = Moderation::add_user_block( $user_id, Moderation::TYPE_ACTOR, $actor_uri );
if ( ! $result ) {
return new \WP_Error(
'activitypub_block_failed',
\__( 'Failed to block the actor.', 'activitypub' ),
array( 'status' => 500 )
);
}
// Block activities should only be sent to the blocked actor.
$data['to'] = array( $actor_uri );
unset( $data['cc'] );
return add_to_outbox( $data, 'Block', $user_id, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE );
}
}

View File

@ -0,0 +1,121 @@
<?php
/**
* Outbox Create handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler\Outbox;
use Activitypub\Collection\Interactions;
use Activitypub\Collection\Posts;
use function Activitypub\is_activity_public;
use function Activitypub\is_activity_reply;
use function Activitypub\is_quote_activity;
/**
* Handle outgoing Create activities (C2S).
*/
class Create {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'activitypub_outbox_create', array( self::class, 'handle_create' ), 10, 3 );
}
/**
* Handle outgoing "Create" activities from local actors.
*
* Creates WordPress content and adds to outbox for federation.
*
* @param array $activity The activity data.
* @param int $user_id The local user ID.
* @param string|null $visibility Content visibility.
*
* @return int|\WP_Error|false The outbox ID on success, WP_Error on failure, false if not handled.
*/
public static function handle_create( $activity, $user_id = null, $visibility = null ) {
// Skip private/direct activities.
if ( ! is_activity_public( $activity ) ) {
return false;
}
$object = $activity['object'] ?? array();
if ( ! \is_array( $object ) ) {
return new \WP_Error( 'invalid_object', 'Invalid object in activity.' );
}
$object_type = $object['type'] ?? '';
// Only handle Note and Article types for now.
if ( ! \in_array( $object_type, array( 'Note', 'Article' ), true ) ) {
return false;
}
if ( is_activity_reply( $activity ) ) {
return self::create_comment( $activity, $user_id );
}
// TODO: Handle quotes differently.
if ( is_quote_activity( $activity ) ) {
return false;
}
return self::create_post( $activity, $user_id, $visibility );
}
/**
* Handle outgoing post from local actor.
*
* Creates a WordPress post. The scheduler will add it to the outbox.
*
* @param array $activity The activity data.
* @param int $user_id The local user ID.
* @param string|null $visibility Content visibility.
*
* @return \WP_Post|\WP_Error The created post on success, WP_Error on failure.
*/
private static function create_post( $activity, $user_id, $visibility ) {
$post = Posts::create( $activity, $user_id, $visibility );
if ( \is_wp_error( $post ) ) {
return $post;
}
/**
* Fires after a post has been created from an outgoing Create activity.
*
* @param int $post_id The created post ID.
* @param array $activity The activity data.
* @param int $user_id The user ID.
* @param string $visibility The content visibility.
*/
\do_action( 'activitypub_outbox_created_post', $post->ID, $activity, $user_id, $visibility );
return $post;
}
/**
* Handle outgoing reply from local actor.
*
* Creates a WordPress comment on the local post. The comment scheduler
* will add it to the outbox and federate it.
*
* @param array $activity The activity data.
* @param int $user_id The local user ID.
*
* @return \WP_Comment|false Comment on success, false if not a local reply.
*/
private static function create_comment( $activity, $user_id ) {
$result = Interactions::add_comment( $activity, $user_id );
if ( ! $result ) {
return false;
}
return \get_comment( $result );
}
}

View File

@ -0,0 +1,130 @@
<?php
/**
* Outbox Delete handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler\Outbox;
use Activitypub\Collection\Remote_Posts;
use function Activitypub\object_to_uri;
use function Activitypub\url_to_commentid;
/**
* Handle outgoing Delete activities.
*/
class Delete {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'activitypub_outbox_delete', array( self::class, 'handle_delete' ), 10, 2 );
}
/**
* Handle outgoing "Delete" activities from local actors.
*
* Deletes a WordPress post or comment.
*
* @param array $data The activity data array.
* @param int $user_id The user ID.
*
* @return \WP_Post|\WP_Comment|false The deleted object, or false on failure.
*/
public static function handle_delete( $data, $user_id = null ) {
$object_id = object_to_uri( $data['object'] ?? '' );
if ( empty( $object_id ) ) {
return false;
}
// Try to delete a comment first, then fall back to a post.
$result = self::maybe_delete_comment( $object_id, $user_id );
if ( ! $result ) {
$result = self::maybe_delete_post( $object_id, $user_id );
}
if ( $result ) {
/**
* Fires after content has been deleted via an outgoing Delete activity.
*
* @param \WP_Post|\WP_Comment $result The deleted object.
* @param array $data The activity data.
* @param int $user_id The user ID.
*/
\do_action( 'activitypub_outbox_handled_delete', $result, $data, $user_id );
}
return $result;
}
/**
* Try to delete a comment by its ActivityPub ID.
*
* @param string $object_id The ActivityPub object ID (URL).
* @param int $user_id The user ID.
*
* @return \WP_Comment|false The deleted comment, or false on failure.
*/
private static function maybe_delete_comment( $object_id, $user_id ) {
$comment_id = url_to_commentid( $object_id );
if ( ! $comment_id ) {
return false;
}
$comment = \get_comment( $comment_id );
if ( ! $comment ) {
return false;
}
// Verify the user owns this comment.
if ( (int) $comment->user_id !== $user_id && $user_id > 0 ) {
return false;
}
if ( \wp_trash_comment( $comment ) ) {
return $comment;
}
return false;
}
/**
* Try to delete a post by its ActivityPub ID.
*
* @param string $object_id The ActivityPub object ID (URL).
* @param int $user_id The user ID.
*
* @return \WP_Post|false The deleted post, or false on failure.
*/
private static function maybe_delete_post( $object_id, $user_id ) {
// Try to find a local post by permalink.
$post_id = \url_to_postid( $object_id );
$post = $post_id ? \get_post( $post_id ) : null;
// Fall back to Posts collection for remote posts (ap_post type).
if ( ! $post instanceof \WP_Post ) {
$post = Remote_Posts::get_by_guid( $object_id );
}
if ( ! $post instanceof \WP_Post ) {
return false;
}
// Verify the user owns this post.
if ( (int) $post->post_author !== $user_id && $user_id > 0 ) {
return false;
}
if ( \wp_trash_post( $post->ID ) ) {
return $post;
}
return false;
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* Outbox Follow handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler\Outbox;
use function Activitypub\follow;
use function Activitypub\object_to_uri;
/**
* Handle outgoing Follow activities.
*/
class Follow {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'activitypub_outbox_follow', array( self::class, 'handle_follow' ), 10, 2 );
}
/**
* Handle outgoing "Follow" activities from local actors.
*
* Delegates to the follow() function which handles pending state,
* proper activity addressing, and adding to the outbox.
*
* @param array $data The activity data array.
* @param int $user_id The user ID.
*
* @return int|\WP_Error The outbox post ID on success, or WP_Error on failure.
*/
public static function handle_follow( $data, $user_id = null ) {
$object = object_to_uri( $data['object'] ?? '' );
if ( empty( $object ) ) {
return $data;
}
return follow( $object, $user_id );
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* Outbox Like handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler\Outbox;
use function Activitypub\object_to_uri;
/**
* Handle outgoing Like activities.
*/
class Like {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'activitypub_outbox_like', array( self::class, 'handle_like' ), 10, 2 );
}
/**
* Handle outgoing "Like" activities from local actors.
*
* Records a like from the local user on remote content.
*
* @param array $data The activity data array.
* @param int $user_id The user ID.
*/
public static function handle_like( $data, $user_id = null ) {
$object_url = object_to_uri( $data['object'] ?? '' );
if ( empty( $object_url ) ) {
return $data;
}
/**
* Fires after an outgoing Like activity has been processed.
*
* @param string $object_url The URL of the liked object.
* @param array $data The activity data.
* @param int $user_id The user ID.
*/
\do_action( 'activitypub_outbox_like_sent', $object_url, $data, $user_id );
return $data;
}
}

View File

@ -0,0 +1,95 @@
<?php
/**
* Outbox Remove handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler\Outbox;
use Activitypub\Collection\Actors;
use function Activitypub\object_to_uri;
/**
* Handle outgoing Remove activities.
*
* Supports removing objects from an actor's featured collection
* by unsticking the corresponding WordPress post.
*/
class Remove {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'activitypub_outbox_remove', array( self::class, 'handle_remove' ), 10, 2 );
}
/**
* Handle outgoing "Remove" activities from local actors.
*
* When the target is the actor's featured collection, the referenced
* post is unstuck. The unstick action triggers the scheduler which
* creates the outbox entry automatically.
*
* @since 8.1.0
*
* @param array $data The activity data array.
* @param int $user_id The user ID.
*
* @return \WP_Post|\WP_Error|array The post object on success, WP_Error on failure, or original data if unhandled.
*/
public static function handle_remove( $data, $user_id = null ) {
$object_uri = object_to_uri( $data['object'] ?? '' );
$target = object_to_uri( $data['target'] ?? '' );
if ( empty( $object_uri ) || empty( $target ) ) {
return $data;
}
$actor = Actors::get_by_id( $user_id );
if ( \is_wp_error( $actor ) ) {
return $actor;
}
// Only handle featured collection targets.
if ( $target !== $actor->get_featured() ) {
return $data;
}
$post_id = \url_to_postid( $object_uri );
if ( ! $post_id ) {
return new \WP_Error(
'activitypub_object_not_found',
\__( 'The referenced object was not found.', 'activitypub' ),
array( 'status' => 404 )
);
}
$post = \get_post( $post_id );
if ( ! $post ) {
return new \WP_Error(
'activitypub_object_not_found',
\__( 'The referenced object was not found.', 'activitypub' ),
array( 'status' => 404 )
);
}
// Verify the user owns this post.
if ( $user_id > 0 && (int) $post->post_author !== $user_id ) {
return new \WP_Error(
'activitypub_forbidden',
\__( 'You can only unfeature your own posts.', 'activitypub' ),
array( 'status' => 403 )
);
}
// Unsticking the post triggers the scheduler which adds to outbox.
\unstick_post( $post_id );
return $post;
}
}

View File

@ -0,0 +1,116 @@
<?php
/**
* Outbox Undo handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler\Outbox;
use Activitypub\Collection\Actors;
use Activitypub\Collection\Outbox as Outbox_Collection;
use Activitypub\Moderation;
use function Activitypub\object_to_uri;
use function Activitypub\unfollow;
/**
* Handle outgoing Undo activities.
*/
class Undo {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'activitypub_outbox_undo', array( self::class, 'handle_undo' ), 10, 2 );
}
/**
* Handle outgoing "Undo" activities from local actors.
*
* Resolves the referenced activity from the outbox and delegates
* to the appropriate collection method to reverse its side effects
* and create the Undo activity.
*
* @param array $data The activity data array.
* @param int $user_id The user ID.
*
* @return int|\WP_Error The undo outbox item ID, or WP_Error on failure.
*/
public static function handle_undo( $data, $user_id = null ) {
$object = $data['object'] ?? '';
$id = object_to_uri( $object );
if ( empty( $id ) ) {
/*
* The embedded object has no `id` — common for clients that
* inline the activity to undo. Mastodon and other major
* implementations match an id-less Undo→Follow on the inner
* Follow's target. Mirror that fallback here so spec-valid
* bodies don't bypass the local unfollow logic.
*/
if ( \is_array( $object ) && 'Follow' === ( $object['type'] ?? '' ) ) {
$embedded_actor = object_to_uri( $object['actor'] ?? '' );
$user_actor = Actors::get_by_id( $user_id );
if ( \is_wp_error( $user_actor ) || ! $embedded_actor || $embedded_actor !== $user_actor->get_id() ) {
return new \WP_Error(
'activitypub_forbidden',
\__( 'You can only undo your own activities.', 'activitypub' ),
array( 'status' => 403 )
);
}
$target = object_to_uri( $object['object'] ?? '' );
if ( $target ) {
return unfollow( $target, $user_id );
}
}
return $data;
}
$outbox_item = Outbox_Collection::get_by_guid( $id );
if ( \is_wp_error( $outbox_item ) ) {
return $data;
}
// Verify the user owns this outbox item (blog actor user_id === 0 can undo any).
if ( $user_id > 0 && (int) $outbox_item->post_author !== $user_id ) {
return new \WP_Error(
'activitypub_forbidden',
\__( 'You can only undo your own activities.', 'activitypub' ),
array( 'status' => 403 )
);
}
$activity_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true );
switch ( $activity_type ) {
case 'Follow':
$stored = \json_decode( $outbox_item->post_content, true );
$target = object_to_uri( $stored['object'] ?? '' );
if ( $target ) {
return unfollow( $target, $user_id );
}
return $data;
case 'Block':
$stored = \json_decode( $outbox_item->post_content, true );
$actor_uri = \is_array( $stored ) ? object_to_uri( $stored['object'] ?? '' ) : '';
if ( $actor_uri ) {
Moderation::remove_user_block( $user_id, Moderation::TYPE_ACTOR, $actor_uri );
}
return Outbox_Collection::undo( $outbox_item );
default:
return Outbox_Collection::undo( $outbox_item );
}
}
}

View File

@ -0,0 +1,114 @@
<?php
/**
* Outbox Update handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler\Outbox;
use Activitypub\Collection\Posts;
use Activitypub\Collection\Remote_Posts;
use function Activitypub\is_activity_public;
/**
* Handle outgoing Update activities (C2S).
*/
class Update {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'activitypub_outbox_update', array( self::class, 'handle_update' ), 10, 3 );
}
/**
* Handle outgoing "Update" activities from local actors.
*
* Updates a WordPress post from the ActivityPub object. The post scheduler
* will add it to the outbox and federate it.
*
* @param array $activity The activity data.
* @param int $user_id The local user ID.
* @param string|null $visibility Content visibility.
*
* @return \WP_Post|\WP_Error|false The updated post on success, WP_Error on failure, false if not handled.
*/
public static function handle_update( $activity, $user_id = null, $visibility = null ) {
// Skip private/direct activities.
if ( ! is_activity_public( $activity ) ) {
return false;
}
$object = $activity['object'] ?? array();
if ( ! \is_array( $object ) ) {
return false;
}
$type = $object['type'] ?? '';
// Only handle Note and Article types.
if ( ! \in_array( $type, array( 'Note', 'Article' ), true ) ) {
return false;
}
$object_id = $object['id'] ?? '';
if ( empty( $object_id ) ) {
return false;
}
/*
* Find the post by its ActivityPub ID.
* First try to find a local post by permalink.
*/
$post_id = \url_to_postid( $object_id );
$post = $post_id ? \get_post( $post_id ) : null;
// Fall back to Posts collection for remote posts (ap_post type).
if ( ! $post instanceof \WP_Post ) {
$post = Remote_Posts::get_by_guid( $object_id );
}
if ( ! $post instanceof \WP_Post ) {
return false;
}
/*
* Verify the user owns this post.
* The blog actor ($user_id === 0) can update any post since it
* represents the site itself.
*/
if ( (int) $post->post_author !== $user_id && $user_id > 0 ) {
return false;
}
// Verify the user has permission to edit this post.
if ( $user_id > 0 && ! \user_can( $user_id, 'edit_post', $post->ID ) ) {
return new \WP_Error(
'activitypub_forbidden',
\__( 'You do not have permission to edit this post.', 'activitypub' ),
array( 'status' => 403 )
);
}
$post = Posts::update( $post, $activity, $visibility );
if ( \is_wp_error( $post ) ) {
return $post;
}
/**
* Fires after a post has been updated from an outgoing Update activity.
*
* @param int $post_id The updated post ID.
* @param array $activity The activity data.
* @param int $user_id The user ID.
*/
\do_action( 'activitypub_outbox_updated_post', $post->ID, $activity, $user_id );
return $post;
}
}