PluginProbe
ActivityPub / 9.2.2
ActivityPub v9.2.2
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 / functions-activity.php

functions-activity.php in ActivityPub 9.2.2, at includes/functions-activity.php

423 lines 12.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Activity functions.
4 *
5 * Functions for working with ActivityPub activities, objects, and actors.
6 *
7 * @package Activitypub
8 */
9
10 namespace Activitypub;
11
12 use Activitypub\Activity\Activity;
13 use Activitypub\Activity\Actor;
14 use Activitypub\Activity\Base_Object;
15
16 /**
17 * Returns the ActivityPub default JSON-context.
18 *
19 * @return array The activitypub context.
20 *
21 * @deprecated 7.6.0 Use the respective context function instead.
22 */
23 function get_context() {
24 \_deprecated_function( __FUNCTION__, '7.6.0', 'Use the respective context function instead.' );
25
26 $context = Activity::JSON_LD_CONTEXT;
27
28 /**
29 * Filters the ActivityPub JSON-LD context.
30 *
31 * This filter allows developers to modify or extend the JSON-LD context used
32 * in ActivityPub responses. The context defines the vocabulary and terms used
33 * in the ActivityPub JSON objects.
34 *
35 * @param array $context The default ActivityPub JSON-LD context array.
36 */
37 return \apply_filters( 'activitypub_json_context', $context );
38 }
39
40 /**
41 * Extract recipient URLs from Activity object.
42 *
43 * @param array $data The Activity object as array.
44 *
45 * @return array The list of user URLs.
46 */
47 function extract_recipients_from_activity( $data ) {
48 $recipient_items = array();
49
50 foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
51 $recipient_items = \array_merge( $recipient_items, extract_recipients_from_activity_property( $i, $data ) );
52 }
53
54 // An Accept/Reject that wraps a Follow is addressed only through the embedded Follow's actor.
55 if (
56 \in_array( $data['type'], array( 'Accept', 'Reject' ), true ) &&
57 ! empty( $data['object'] ) &&
58 \is_array( $data['object'] ) &&
59 ! empty( $data['object']['actor'] )
60 ) {
61 $recipient_items[] = object_to_uri( $data['object']['actor'] );
62 }
63
64 return \array_unique( \array_filter( $recipient_items ) );
65 }
66
67 /**
68 * Extract recipient URLs from a specific property of an Activity object.
69 *
70 * Checks the activity level first, then falls back to the object property,
71 * and finally checks the instrument property (used by QuoteRequest activities).
72 *
73 * @param string $property The property to extract recipients from (e.g., 'to', 'cc').
74 * @param array $data The Activity object as array.
75 *
76 * @return array The list of user URLs.
77 */
78 function extract_recipients_from_activity_property( $property, $data ) {
79 $recipients = array();
80
81 if ( ! empty( $data[ $property ] ) ) {
82 $recipients = $data[ $property ];
83 } elseif ( ! empty( $data['object'][ $property ] ) ) {
84 $recipients = $data['object'][ $property ];
85 } elseif ( ! empty( $data['instrument'][ $property ] ) ) {
86 // QuoteRequest activities have addressing in the instrument (the quoting Note).
87 $recipients = $data['instrument'][ $property ];
88 }
89
90 $recipients = \array_map( '\Activitypub\object_to_uri', (array) $recipients );
91
92 return \array_unique( \array_filter( $recipients ) );
93 }
94
95 /**
96 * Determine the visibility of the activity based on its recipients.
97 *
98 * @param array $activity The activity data.
99 *
100 * @return string One of ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC,
101 * ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, or
102 * ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE.
103 */
104 function get_activity_visibility( $activity ) {
105 // Set default visibility for specific activity types.
106 if ( ! empty( $activity['type'] ) && \in_array( $activity['type'], array( 'Accept', 'Delete', 'Follow', 'Reject', 'Undo' ), true ) ) {
107 return ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE;
108 }
109
110 // Check 'to' field for public visibility.
111 $to = extract_recipients_from_activity_property( 'to', $activity );
112 if ( ! empty( \array_intersect( $to, ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS ) ) ) {
113 return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC;
114 }
115
116 // Check 'cc' field for quiet public visibility.
117 $cc = extract_recipients_from_activity_property( 'cc', $activity );
118 if ( ! empty( \array_intersect( $cc, ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS ) ) ) {
119 return ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC;
120 }
121
122 return ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE;
123 }
124
125 /**
126 * Check if passed Activity is Public.
127 *
128 * @see https://github.com/w3c/activitypub/issues/404#issuecomment-2926310561
129 * @see https://www.w3.org/TR/activitypub/#delivery (Section 7.1, "Silent and private activities")
130 *
131 * @param Base_Object|array $data The Activity object as Base_Object or array.
132 *
133 * @return boolean True if public, false if not.
134 */
135 function is_activity_public( $data ) {
136 if ( $data instanceof Base_Object ) {
137 $data = $data->to_array();
138 }
139
140 $recipients = extract_recipients_from_activity( $data );
141
142 if ( empty( $recipients ) ) {
143 return false;
144 }
145
146 return ! empty( \array_intersect( $recipients, ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS ) );
147 }
148
149 /**
150 * Check if passed Activity is a reply.
151 *
152 * @param array $data The Activity object as array.
153 *
154 * @return boolean True if a reply, false if not.
155 */
156 function is_activity_reply( $data ) {
157 return ! empty( $data['object']['inReplyTo'] );
158 }
159
160 /**
161 * Check if passed Activity is a quote.
162 *
163 * Checks for quote properties: quote, quoteUrl, quoteUri, or _misskey_quote.
164 *
165 * @param array $data The Activity object as array.
166 *
167 * @return boolean True if a quote, false if not.
168 */
169 function is_quote_activity( $data ) {
170 return ! empty( $data['object']['quote'] ) ||
171 ! empty( $data['object']['quoteUrl'] ) ||
172 ! empty( $data['object']['quoteUri'] ) ||
173 ! empty( $data['object']['_misskey_quote'] );
174 }
175
176 /**
177 * Get the URI of an ActivityPub object.
178 *
179 * @param array|string $data The ActivityPub object.
180 *
181 * @return string|null The URI of the ActivityPub object.
182 */
183 function object_to_uri( $data ) {
184 // Check whether it is already simple.
185 if ( ! $data || \is_string( $data ) ) {
186 return $data;
187 }
188
189 if ( \is_object( $data ) ) {
190 $data = $data->to_array();
191 }
192
193 /*
194 * Check if it is a list, then take first item.
195 * This plugin does not support collections.
196 */
197 if ( \array_is_list( $data ) ) {
198 $data = $data[0];
199 }
200
201 // Check if it is simplified now.
202 if ( \is_string( $data ) ) {
203 return $data;
204 }
205
206 $type = 'Object';
207 if ( isset( $data['type'] ) ) {
208 $type = $data['type'];
209 }
210
211 // Return part of Object that makes most sense.
212 switch ( $type ) {
213 case 'Audio': // See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio.
214 case 'Document': // See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document.
215 case 'Image': // See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image.
216 case 'Video': // See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video.
217 $data = object_to_uri( $data['url'] );
218 break;
219
220 case 'Link': // See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-link.
221 case 'Mention': // See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-mention.
222 $data = $data['href'];
223 break;
224
225 case 'FeaturedItem': // See https://github.com/mastodon/featured_collections/pull/1.
226 $data = object_to_uri( $data['featuredObject'] ?? null );
227 break;
228
229 default:
230 if ( isset( $data['id'] ) ) {
231 $data = $data['id'];
232 } elseif ( isset( $data['url'] ) ) {
233 $data = object_to_uri( $data['url'] );
234 } elseif ( isset( $data['href'] ) ) {
235 $data = $data['href'];
236 } else {
237 $data = null;
238 }
239 break;
240 }
241
242 return $data;
243 }
244
245 /**
246 * Check whether two references point at the same actor.
247 *
248 * Both values are resolved to their canonical URI via object_to_uri() before
249 * comparison. Empty references never match, so a missing actor can never be
250 * mistaken for a match.
251 *
252 * @param array|object|string $a The first actor reference.
253 * @param array|object|string $b The second actor reference.
254 *
255 * @return bool True when both resolve to the same non-empty URI.
256 */
257 function is_same_actor( $a, $b ) {
258 $a = object_to_uri( $a );
259 $b = object_to_uri( $b );
260
261 return ! empty( $a ) && ! empty( $b ) && $a === $b;
262 }
263
264 /**
265 * Check whether two references live on the same host.
266 *
267 * Both values are resolved to their canonical URI via object_to_uri(), then
268 * their hosts are compared case-insensitively. Empty references, or references
269 * without a host, never match.
270 *
271 * @param array|object|string $a The first reference.
272 * @param array|object|string $b The second reference.
273 *
274 * @return bool True when both resolve to a URI on the same host.
275 */
276 function is_same_host( $a, $b ) {
277 $host_a = \wp_parse_url( (string) object_to_uri( $a ), PHP_URL_HOST );
278 $host_b = \wp_parse_url( (string) object_to_uri( $b ), PHP_URL_HOST );
279
280 return ! empty( $host_a ) && ! empty( $host_b ) && \strtolower( $host_a ) === \strtolower( $host_b );
281 }
282
283 /**
284 * Whether an object is served under its own canonical id.
285 *
286 * An object is only trustworthy to cache when its own `id` is the URL it was
287 * actually served from: otherwise one host could serve a document — and its
288 * public key — under another host's id. Reads the raw `id` attribute only
289 * (never the `url`/`href` fallback that object_to_uri() applies), because the
290 * cache is keyed on `id`, so "is this canonical?" must ask the same field the
291 * write uses. The comparison ignores the URL fragment and a trailing slash;
292 * everything else (scheme, host, port, path, query) must match exactly.
293 * Host-level equality is deliberately NOT enough — any different id on the same
294 * host is still a distinct cache entry that a document served elsewhere must not write.
295 *
296 * @param array|string $item The fetched object, or its id.
297 * @param string $url The URL the object was served from.
298 *
299 * @return bool True when the object's id is the canonical URL it was served from.
300 */
301 function id_matches_url( $item, $url ) {
302 if ( \is_array( $item ) ) {
303 $id = isset( $item['id'] ) && \is_string( $item['id'] ) ? $item['id'] : '';
304 } elseif ( \is_string( $item ) ) {
305 $id = $item;
306 } else {
307 $id = '';
308 }
309
310 $id = \strip_fragment_from_url( $id );
311 $url = \strip_fragment_from_url( (string) $url );
312
313 if ( '' === $id || '' === $url ) {
314 return false;
315 }
316
317 return \untrailingslashit( $id ) === \untrailingslashit( $url );
318 }
319
320 /**
321 * Check if an `$data` is an Activity.
322 *
323 * @see https://www.w3.org/ns/activitystreams#activities
324 *
325 * @param array|object|string $data The data to check.
326 *
327 * @return boolean True if the `$data` is an Activity, false otherwise.
328 */
329 function is_activity( $data ) {
330 /**
331 * Filters the activity types.
332 *
333 * @param array $types The activity types.
334 */
335 $types = \apply_filters( 'activitypub_activity_types', Activity::TYPES );
336
337 return _is_type_of( $data, $types );
338 }
339
340 /**
341 * Check if an `$data` is an Activity Object.
342 *
343 * @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
344 *
345 * @param array|object|string $data The data to check.
346 *
347 * @return boolean True if the `$data` is an Activity Object, false otherwise.
348 */
349 function is_activity_object( $data ) {
350 /**
351 * Filters the activity object types.
352 *
353 * @param array $types The activity object types.
354 */
355 $types = \apply_filters( 'activitypub_activity_object_types', Base_Object::TYPES );
356
357 return _is_type_of( $data, $types );
358 }
359
360 /**
361 * Check if an `$data` is an Actor.
362 *
363 * @see https://www.w3.org/ns/activitystreams#actor
364 *
365 * @param array|object|string $data The data to check.
366 *
367 * @return boolean True if the `$data` is an Actor, false otherwise.
368 */
369 function is_actor( $data ) {
370 /**
371 * Filters the actor types.
372 *
373 * @param array $types The actor types.
374 */
375 $types = \apply_filters( 'activitypub_actor_types', Actor::TYPES );
376
377 return _is_type_of( $data, $types );
378 }
379
380 /**
381 * Check if an `$data` is a Collection.
382 *
383 * @see https://www.w3.org/ns/activitystreams#collections
384 *
385 * @param array|object|string $data The data to check.
386 *
387 * @return boolean True if the `$data` is a Collection, false otherwise.
388 */
389 function is_collection( $data ) {
390 /**
391 * Filters the collection types.
392 *
393 * @param array $types The collection types.
394 */
395 $types = \apply_filters( 'activitypub_collection_types', array( 'Collection', 'OrderedCollection', 'CollectionPage', 'OrderedCollectionPage' ) );
396
397 return _is_type_of( $data, $types );
398 }
399
400 /**
401 * Private helper to check if $data is of a given type set.
402 *
403 * @param array|object|string $data The data to check.
404 * @param array $types The types to check against.
405 *
406 * @return boolean True if $data is of one of the types, false otherwise.
407 */
408 function _is_type_of( $data, $types ) {
409 if ( \is_string( $data ) ) {
410 return \in_array( $data, $types, true );
411 }
412
413 if ( \is_array( $data ) && isset( $data['type'] ) ) {
414 return \in_array( $data['type'], $types, true );
415 }
416
417 if ( $data instanceof Base_Object ) {
418 return \in_array( $data->get_type(), $types, true );
419 }
420
421 return false;
422 }
423