PluginProbe
ActivityPub / 7.8.4
ActivityPub v7.8.4
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 7.8.4, at includes/handler/class-quote-request.php

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