| 1 |
<?php |
| 2 |
/** |
| 3 |
* Follow handler file. |
| 4 |
* |
| 5 |
* @package Activitypub |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace Activitypub\Handler; |
| 9 |
|
| 10 |
use Activitypub\Activity\Activity; |
| 11 |
use Activitypub\Application; |
| 12 |
use Activitypub\Collection\Actors; |
| 13 |
use Activitypub\Collection\Followers; |
| 14 |
use Activitypub\Collection\Remote_Actors; |
| 15 |
use Activitypub\Http; |
| 16 |
|
| 17 |
use function Activitypub\add_to_outbox; |
| 18 |
use function Activitypub\object_to_uri; |
| 19 |
|
| 20 |
/** |
| 21 |
* Handle Follow requests. |
| 22 |
*/ |
| 23 |
class Follow { |
| 24 |
/** |
| 25 |
* Initialize the class, registering WordPress hooks. |
| 26 |
*/ |
| 27 |
public static function init() { |
| 28 |
\add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow' ), 10, 2 ); |
| 29 |
\add_action( 'activitypub_handled_follow', array( self::class, 'queue_accept' ), 10, 4 ); |
| 30 |
|
| 31 |
// The Application actor cannot be followed; explicitly reject such Follows so they don't sit "pending" on the remote instance forever. |
| 32 |
\add_action( 'activitypub_inbox_shared_follow', array( self::class, 'reject_application_follow' ), 10, 2 ); |
| 33 |
} |
| 34 |
|
| 35 |
/** |
| 36 |
* Handle "Follow" requests. |
| 37 |
* |
| 38 |
* @param array $activity The activity object. |
| 39 |
* @param int|int[] $user_ids The user ID(s). |
| 40 |
*/ |
| 41 |
public static function handle_follow( $activity, $user_ids ) { |
| 42 |
// Extract the user ID (follow requests are always for a single user). |
| 43 |
$user_id = \is_array( $user_ids ) ? \reset( $user_ids ) : $user_ids; |
| 44 |
|
| 45 |
// Check if the actor already follows the user. |
| 46 |
$already_following = false; |
| 47 |
$remote_actor = Remote_Actors::get_by_uri( $activity['actor'] ); |
| 48 |
if ( ! \is_wp_error( $remote_actor ) ) { |
| 49 |
$already_following = Followers::follows( $remote_actor->ID, $user_id ); |
| 50 |
} |
| 51 |
|
| 52 |
// Save follower if not already following. |
| 53 |
if ( $already_following ) { |
| 54 |
$success = false; |
| 55 |
} else { |
| 56 |
$remote_actor = Followers::add( $user_id, $activity['actor'] ); |
| 57 |
$success = ! \is_wp_error( $remote_actor ); |
| 58 |
|
| 59 |
if ( $success ) { |
| 60 |
$remote_actor = \get_post( $remote_actor ); |
| 61 |
} |
| 62 |
} |
| 63 |
|
| 64 |
/** |
| 65 |
* Fires after a new follower has been added. |
| 66 |
* |
| 67 |
* @deprecated 7.5.0 Use "activitypub_handled_follow" instead. |
| 68 |
* |
| 69 |
* @param string $actor The URL of the actor (follower) who initiated the follow. |
| 70 |
* @param array $activity The complete activity data of the follow request. |
| 71 |
* @param int $user_id The ID of the WordPress user being followed. |
| 72 |
* @param \WP_Post|\WP_Error $remote_actor The Actor object containing the new follower's data. |
| 73 |
*/ |
| 74 |
\do_action_deprecated( 'activitypub_followers_post_follow', array( $activity['actor'], $activity, $user_id, $remote_actor ), '7.5.0', 'activitypub_handled_follow' ); |
| 75 |
|
| 76 |
/** |
| 77 |
* Fires after a Follow activity has been handled. |
| 78 |
* |
| 79 |
* @param array $activity The ActivityPub activity data. |
| 80 |
* @param int[] $user_ids The local user IDs. |
| 81 |
* @param bool $success True on success, false otherwise. |
| 82 |
* @param \WP_Post|\WP_Error $remote_actor The remote actor/follower, or WP_Error if failed. |
| 83 |
*/ |
| 84 |
\do_action( 'activitypub_handled_follow', $activity, (array) $user_ids, $success, $remote_actor ); |
| 85 |
} |
| 86 |
|
| 87 |
/** |
| 88 |
* Send Accept response. |
| 89 |
* |
| 90 |
* @param array $activity_object The ActivityPub activity data. |
| 91 |
* @param int|int[] $user_ids The local user IDs. |
| 92 |
* @param bool $success True on success, false otherwise. |
| 93 |
* @param \WP_Post|\WP_Error $remote_actor The remote actor/follower, or WP_Error if failed. |
| 94 |
*/ |
| 95 |
public static function queue_accept( $activity_object, $user_ids, $success, $remote_actor ) { |
| 96 |
if ( \is_wp_error( $remote_actor ) ) { |
| 97 |
// Impossible to send a "Reject" because we can not get the Remote-Inbox. |
| 98 |
return; |
| 99 |
} |
| 100 |
|
| 101 |
// Extract the user ID from the array (follow requests are always for a single user). |
| 102 |
$user_id = \is_array( $user_ids ) ? \reset( $user_ids ) : $user_ids; |
| 103 |
|
| 104 |
$actor = $activity_object['actor']; |
| 105 |
|
| 106 |
// Only send minimal data. |
| 107 |
$activity_object = \array_intersect_key( |
| 108 |
$activity_object, |
| 109 |
array( |
| 110 |
'id' => 1, |
| 111 |
'type' => 1, |
| 112 |
'actor' => 1, |
| 113 |
'object' => 1, |
| 114 |
) |
| 115 |
); |
| 116 |
|
| 117 |
$activity = new Activity(); |
| 118 |
$activity->set_type( 'Accept' ); |
| 119 |
$activity->set_actor( Actors::get_by_id( $user_id )->get_id() ); |
| 120 |
$activity->set_object( $activity_object ); |
| 121 |
$activity->set_to( array( $actor ) ); |
| 122 |
|
| 123 |
add_to_outbox( $activity, null, $user_id, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); |
| 124 |
} |
| 125 |
|
| 126 |
/** |
| 127 |
* Reject Follow requests aimed at the Application actor. |
| 128 |
* |
| 129 |
* The Application advertises `manuallyApprovesFollowers` but is not followable, |
| 130 |
* so an explicit Reject is the only way a remote follow request gets resolved. |
| 131 |
* The Reject is sent directly instead of through the Outbox, which only |
| 132 |
* dispatches for real actors, and is signed with the Application key. |
| 133 |
* |
| 134 |
* @since 9.1.0 |
| 135 |
* |
| 136 |
* @param array $activity The Follow activity data. |
| 137 |
* @param int[] $user_ids The local recipient IDs the inbox resolved. |
| 138 |
*/ |
| 139 |
public static function reject_application_follow( $activity, $user_ids ) { |
| 140 |
// A resolved recipient means the Follow targets a real actor, not the Application. |
| 141 |
if ( ! empty( $user_ids ) ) { |
| 142 |
return; |
| 143 |
} |
| 144 |
|
| 145 |
if ( empty( $activity['object'] ) || ! Application::is_application_resource( object_to_uri( $activity['object'] ) ) ) { |
| 146 |
return; |
| 147 |
} |
| 148 |
|
| 149 |
$actor = object_to_uri( $activity['actor'] ); |
| 150 |
$remote_actor = Remote_Actors::fetch_by_uri( $actor ); |
| 151 |
|
| 152 |
if ( \is_wp_error( $remote_actor ) ) { |
| 153 |
return; |
| 154 |
} |
| 155 |
|
| 156 |
$inbox = \get_post_meta( $remote_actor->ID, '_activitypub_inbox', true ); |
| 157 |
|
| 158 |
if ( ! $inbox ) { |
| 159 |
return; |
| 160 |
} |
| 161 |
|
| 162 |
// Only send minimal data. |
| 163 |
$origin_activity = \array_intersect_key( |
| 164 |
$activity, |
| 165 |
array( |
| 166 |
'id' => 1, |
| 167 |
'type' => 1, |
| 168 |
'actor' => 1, |
| 169 |
'object' => 1, |
| 170 |
) |
| 171 |
); |
| 172 |
|
| 173 |
$reject = new Activity(); |
| 174 |
$reject->set_type( 'Reject' ); |
| 175 |
$reject->set_id( Application::get_id() . '#reject-' . \md5( \wp_json_encode( $origin_activity ) ) ); |
| 176 |
$reject->set_actor( Application::get_id() ); |
| 177 |
$reject->set_object( $origin_activity ); |
| 178 |
$reject->set_to( array( $actor ) ); |
| 179 |
|
| 180 |
Http::post( $inbox, $reject->to_json(), null ); |
| 181 |
} |
| 182 |
} |
| 183 |
|