PluginProbe
ActivityPub / 8.2.1
ActivityPub v8.2.1
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / handler / class-quote-request.php

class-quote-request.php in ActivityPub 8.2.1, at includes/handler/class-quote-request.php

337 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handler for QuoteRequest activities.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Handler;
9
10 use Activitypub\Activity\Activity;
11 use Activitypub\Collection\Actors;
12 use Activitypub\Collection\Followers;
13 use Activitypub\Collection\Inbox;
14 use Activitypub\Collection\Remote_Actors;
15
16 use function Activitypub\add_to_outbox;
17 use function Activitypub\object_to_uri;
18 use function Activitypub\user_can_activitypub;
19
20 /**
21 * Handler for QuoteRequest activities.
22 *
23 * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md
24 */
25 class Quote_Request {
26 /**
27 * Initialize the class, registering WordPress hooks.
28 */
29 public static function init() {
30 \add_action( 'activitypub_inbox_quote_request', array( self::class, 'handle_quote_request' ), 10, 2 );
31 \add_action( 'activitypub_rest_inbox_disallowed', array( self::class, 'handle_blocked_request' ), 10, 3 );
32 \add_action( 'delete_comment', array( self::class, 'handle_quote_delete' ), 10, 2 );
33
34 \add_filter( 'activitypub_validate_object', array( self::class, 'validate_object' ), 10, 3 );
35 }
36
37 /**
38 * Handle QuoteRequest activities.
39 *
40 * @param array $activity The activity object.
41 * @param int|int[] $user_ids The user ID(s).
42 */
43 public static function handle_quote_request( $activity, $user_ids ) {
44 $state = true;
45 $post_id = \url_to_postid( object_to_uri( $activity['object'] ) );
46 $post = $post_id ? \get_post( $post_id ) : null;
47
48 if ( ! $post ) {
49 $user_id = \is_array( $user_ids ) ? \reset( $user_ids ) : $user_ids;
50 self::queue_reject( $activity, $user_id );
51 return;
52 }
53
54 // Use the post author as the responding actor — they own the quoted content.
55 $user_id = (int) $post->post_author;
56 $content_policy = \get_post_meta( $post_id, 'activitypub_interaction_policy_quote', true );
57
58 // Fall back to global default if not set.
59 if ( ! $content_policy ) {
60 $content_policy = \get_option( 'activitypub_default_quote_policy', ACTIVITYPUB_INTERACTION_POLICY_ANYONE );
61 }
62
63 switch ( $content_policy ) {
64 case ACTIVITYPUB_INTERACTION_POLICY_ME:
65 self::queue_reject( $activity, $user_id );
66 $state = false;
67 break;
68 case ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS:
69 $follower = Remote_Actors::get_by_uri( object_to_uri( $activity['actor'] ) );
70 if ( ! \is_wp_error( $follower ) && Followers::follows( $follower->ID, $user_id ) ) {
71 self::queue_accept( $activity, $user_id, $post_id );
72 } else {
73 self::queue_reject( $activity, $user_id );
74 $state = false;
75 }
76 break;
77 case ACTIVITYPUB_INTERACTION_POLICY_ANYONE:
78 default:
79 self::queue_accept( $activity, $user_id, $post_id );
80 break;
81 }
82
83 /**
84 * Fires after an ActivityPub QuoteRequest activity has been handled.
85 *
86 * @param array $activity The ActivityPub activity data.
87 * @param int[] $user_ids The local user IDs.
88 * @param bool $success True on success, false otherwise.
89 * @param string $content_policy The content policy for the quoted post.
90 */
91 \do_action( 'activitypub_handled_quote_request', $activity, (array) $user_ids, $state, $content_policy );
92 }
93
94 /**
95 * ActivityPub inbox disallowed activity.
96 *
97 * @param array $activity The activity array.
98 * @param int|int[]|null $user_ids The user ID(s).
99 * @param string $type The type of the activity.
100 */
101 public static function handle_blocked_request( $activity, $user_ids, $type ) {
102 if ( ! in_array( strtolower( $type ), array( 'quoterequest', 'quote_request' ), true ) ) {
103 return;
104 }
105
106 // Extract the user ID (quote requests are always for a single user).
107 $user_id = \is_array( $user_ids ) ? \reset( $user_ids ) : $user_ids;
108
109 self::queue_reject( $activity, $user_id );
110 }
111
112 /**
113 * Handle deletion of a quote comment.
114 *
115 * When a local quote comment is deleted, send a Reject activity to revoke
116 * the previously accepted QuoteRequest.
117 *
118 * @param int $comment_id The comment ID being deleted.
119 * @param \WP_Comment|null $comment The comment object, or null if not available.
120 */
121 public static function handle_quote_delete( $comment_id, $comment ) {
122 // Try to get comment if not provided.
123 if ( ! $comment ) {
124 $comment = \get_comment( $comment_id );
125 }
126
127 // Only handle quote comments.
128 if ( ! $comment || 'quote' !== $comment->comment_type ) {
129 return;
130 }
131
132 // Get the post being quoted.
133 $post_id = $comment->comment_post_ID;
134 if ( ! $post_id ) {
135 return;
136 }
137
138 // Get the instrument URL (the quote post URL) from comment meta.
139 $instrument_url = \get_comment_meta( $comment_id, 'source_url', true );
140 if ( ! $instrument_url ) {
141 $instrument_url = \get_comment_meta( $comment_id, 'source_id', true );
142 }
143
144 if ( ! $instrument_url ) {
145 return;
146 }
147
148 // Get the post author (who accepted the quote).
149 $post = \get_post( $post_id );
150 if ( ! $post || ! $post->post_author ) {
151 return;
152 }
153
154 /*
155 * Try to retrieve the original QuoteRequest from the inbox.
156 * For QuoteRequest activities, the inbox stores the instrument URL
157 * in _activitypub_object_id, so we can query by that.
158 */
159 $activity_object = null;
160 $inbox_item = Inbox::get_by_type_and_object( 'QuoteRequest', $instrument_url );
161
162 if ( $inbox_item instanceof \WP_Post ) {
163 $activity_object = \json_decode( $inbox_item->post_content, true );
164 if ( JSON_ERROR_NONE !== \json_last_error() ) {
165 $activity_object = null;
166 }
167 }
168
169 // Fallback: If inbox item not found, reconstruct from available data.
170 if ( ! $activity_object ) {
171 $activity_object = array(
172 'type' => 'QuoteRequest',
173 'actor' => $comment->comment_author_url,
174 'object' => \get_permalink( $post_id ),
175 'instrument' => $instrument_url,
176 'published' => \gmdate( 'c' ),
177 );
178 }
179
180 // Remove from _activitypub_quoted_by meta.
181 \delete_post_meta( $post_id, '_activitypub_quoted_by', $instrument_url );
182
183 // Send Reject activity to revoke the quote permission.
184 self::queue_reject( $activity_object, $post->post_author );
185
186 /**
187 * Fires after a quote comment has been deleted and Reject activity sent.
188 *
189 * @param int $comment_id The deleted comment ID.
190 * @param int $post_id The post ID that was quoted.
191 * @param string $instrument_url The instrument URL (quote post).
192 * @param array $activity_object The QuoteRequest activity that was rejected.
193 */
194 \do_action( 'activitypub_quote_comment_deleted', $comment_id, $post_id, $instrument_url, $activity_object );
195 }
196
197 /**
198 * Send an Accept activity in response to the QuoteRequest.
199 *
200 * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md#example-accept
201 *
202 * @param array $activity_object The activity object.
203 * @param int $user_id The user ID.
204 * @param int $post_id The post ID.
205 */
206 public static function queue_accept( $activity_object, $user_id, $post_id ) {
207 // Fall back to the blog actor if the user has ActivityPub disabled.
208 if ( ! user_can_activitypub( $user_id ) ) {
209 $user_id = Actors::BLOG_USER_ID;
210 }
211
212 $actor = Actors::get_by_id( $user_id );
213
214 if ( \is_wp_error( $actor ) ) {
215 return;
216 }
217
218 $activity_object['instrument'] = object_to_uri( $activity_object['instrument'] );
219
220 $post_meta = \get_post_meta( $post_id, '_activitypub_quoted_by', false );
221 if ( in_array( $activity_object['instrument'], $post_meta, true ) ) {
222 global $wpdb;
223
224 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
225 $meta_id = $wpdb->get_var(
226 $wpdb->prepare(
227 "SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s AND meta_value = %s LIMIT 1",
228 $post_id,
229 '_activitypub_quoted_by',
230 $activity_object['instrument']
231 )
232 );
233 } else {
234 $meta_id = \add_post_meta( $post_id, '_activitypub_quoted_by', $activity_object['instrument'] );
235 }
236
237 // Only send minimal data.
238 $activity_object = array_intersect_key(
239 $activity_object,
240 array(
241 'id' => 1,
242 'type' => 1,
243 'actor' => 1,
244 'object' => 1,
245 'instrument' => 1,
246 )
247 );
248
249 $url = \add_query_arg(
250 array(
251 'p' => $post_id,
252 'stamp' => $meta_id,
253 ),
254 \home_url( '/' )
255 );
256
257 $activity = new Activity();
258 $activity->set_type( 'Accept' );
259 $activity->set_actor( $actor->get_id() );
260 $activity->set_object( $activity_object );
261 $activity->set_result( $url );
262 $activity->add_to( object_to_uri( $activity_object['actor'] ) );
263
264 add_to_outbox( $activity, null, $user_id, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE );
265 }
266
267 /**
268 * Send a Reject activity in response to the QuoteRequest.
269 *
270 * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md#example-reject
271 *
272 * @param array $activity_object The activity object.
273 * @param int $user_id The user ID.
274 */
275 public static function queue_reject( $activity_object, $user_id ) {
276 // Fall back to the blog actor if the user has ActivityPub disabled.
277 if ( ! user_can_activitypub( $user_id ) ) {
278 $user_id = Actors::BLOG_USER_ID;
279 }
280
281 $actor = Actors::get_by_id( $user_id );
282
283 if ( \is_wp_error( $actor ) ) {
284 return;
285 }
286
287 $activity_object['instrument'] = object_to_uri( $activity_object['instrument'] );
288
289 // Only send minimal data.
290 $activity_object = array_intersect_key(
291 $activity_object,
292 array(
293 'id' => 1,
294 'type' => 1,
295 'actor' => 1,
296 'object' => 1,
297 'instrument' => 1,
298 )
299 );
300
301 $activity = new Activity();
302 $activity->set_type( 'Reject' );
303 $activity->set_actor( $actor->get_id() );
304 $activity->set_object( $activity_object );
305 $activity->add_to( object_to_uri( $activity_object['actor'] ) );
306
307 add_to_outbox( $activity, null, $user_id, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE );
308 }
309
310 /**
311 * Validate the object.
312 *
313 * @param bool $valid The validation state.
314 * @param string $param The object parameter.
315 * @param \WP_REST_Request $request The request object.
316 *
317 * @return bool The validation state: true if valid, false if not.
318 */
319 public static function validate_object( $valid, $param, $request ) {
320 $activity = $request->get_json_params();
321
322 if ( empty( $activity['type'] ) ) {
323 return false;
324 }
325
326 if ( 'QuoteRequest' !== $activity['type'] ) {
327 return $valid;
328 }
329
330 if ( ! isset( $activity['actor'], $activity['object'], $activity['instrument'] ) ) {
331 return false;
332 }
333
334 return $valid;
335 }
336 }
337