PluginProbe
ActivityPub / 9.2.0
ActivityPub v9.2.0
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 / collection / class-actors.php

class-actors.php in ActivityPub 9.2.0, at includes/collection/class-actors.php

620 lines 15.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Actors collection file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Collection;
9
10 use Activitypub\Activity\Actor;
11 use Activitypub\Application;
12 use Activitypub\Model\Blog;
13 use Activitypub\Model\User;
14 use Activitypub\Signature;
15
16 use function Activitypub\is_user_type_disabled;
17 use function Activitypub\normalize_host;
18 use function Activitypub\normalize_url;
19 use function Activitypub\object_to_uri;
20 use function Activitypub\url_to_authorid;
21 use function Activitypub\user_can_activitypub;
22
23 /**
24 * Actors collection.
25 *
26 * Provides methods to retrieve, create, update, and manage ActivityPub actors (users, blogs, applications, and remote actors).
27 */
28 class Actors {
29 /**
30 * The ID of the Blog User.
31 *
32 * @var int
33 */
34 const BLOG_USER_ID = 0;
35
36 /**
37 * The ID of the former Application user.
38 *
39 * @deprecated 9.1.0 The Application is no longer a user-like actor, see {@see \Activitypub\Application}. Retained for backward compatibility and legacy data handling.
40 *
41 * @var int
42 */
43 const APPLICATION_USER_ID = -1;
44
45 /**
46 * Get the Actor by ID.
47 *
48 * @param int $user_id The user ID.
49 *
50 * @return Actor|User|Blog|Application|\WP_Error Actor object or WP_Error if not found or not permitted.
51 */
52 public static function get_by_id( $user_id ) {
53 if ( \is_numeric( $user_id ) ) {
54 $user_id = (int) $user_id;
55 }
56
57 /**
58 * Filter the actor before resolving by ID.
59 *
60 * Allows third-party plugins to register custom virtual actors
61 * resolved by ID, mirroring the `activitypub_pre_get_by_username`
62 * filter for username lookups.
63 *
64 * @since 8.1.0
65 *
66 * @param null $pre The pre-existing value.
67 * @param int $user_id The user ID.
68 */
69 $pre = \apply_filters( 'activitypub_pre_get_by_id', null, $user_id );
70 if ( null !== $pre ) {
71 return $pre;
72 }
73
74 if ( ! user_can_activitypub( $user_id ) ) {
75 return new \WP_Error(
76 'activitypub_user_not_found',
77 \__( 'Actor not found', 'activitypub' ),
78 array( 'status' => 404 )
79 );
80 }
81
82 switch ( $user_id ) {
83 case self::BLOG_USER_ID:
84 return new Blog();
85 default:
86 return User::from_wp_user( $user_id );
87 }
88 }
89
90 /**
91 * Get the Actor by username.
92 *
93 * @param string $username Name of the actor.
94 *
95 * @return User|Blog|Application|\WP_Error Actor object or WP_Error if not found.
96 */
97 public static function get_by_username( $username ) {
98 /**
99 * Filter the username before we do anything else.
100 *
101 * @param null $pre The pre-existing value.
102 * @param string $username The username.
103 */
104 $pre = \apply_filters( 'activitypub_pre_get_by_username', null, $username );
105 if ( null !== $pre ) {
106 return $pre;
107 }
108
109 $id = self::get_id_by_username( $username );
110 if ( \is_wp_error( $id ) ) {
111 return $id;
112 }
113
114 return self::get_by_id( $id );
115 }
116
117 /**
118 * Get the Actor by username.
119 *
120 * @param string $username Name of the actor.
121 *
122 * @return int|\WP_Error Actor id or WP_Error if not found.
123 */
124 public static function get_id_by_username( $username ) {
125 // Check for blog user.
126 if (
127 Blog::get_default_username() === $username ||
128 \get_option( 'activitypub_blog_identifier' ) === $username
129 ) {
130 if ( is_user_type_disabled( 'blog' ) ) {
131 return new \WP_Error(
132 'activitypub_user_not_found',
133 \__( 'Actor not found', 'activitypub' ),
134 array( 'status' => 404 )
135 );
136 }
137
138 return self::BLOG_USER_ID;
139 }
140
141 /*
142 * The 'application' identifier is reserved for the signing-only Application
143 * actor, which is served only through the dedicated /application endpoint.
144 * Never resolve it to a regular user, even on sites that happen to have a
145 * user named "application". Compare lowercased: the user lookups below are
146 * case-insensitive, so the reservation has to be too.
147 */
148 if ( Application::USERNAME === \strtolower( $username ) ) {
149 return new \WP_Error(
150 'activitypub_user_not_found',
151 \__( 'Actor not found', 'activitypub' ),
152 array( 'status' => 404 )
153 );
154 }
155
156 // Check for 'activitypub_username' meta.
157 $user = new \WP_User_Query(
158 array(
159 'count_total' => false,
160 'number' => 1,
161 'hide_empty' => true,
162 'fields' => 'ID',
163 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
164 'meta_query' => array(
165 'relation' => 'OR',
166 array(
167 'key' => '_activitypub_user_identifier',
168 'value' => $username,
169 'compare' => 'LIKE',
170 ),
171 ),
172 )
173 );
174
175 if ( $user->get_results() ) {
176 return \current( $user->get_results() );
177 }
178
179 $username = \str_replace( array( '*', '%' ), '', $username );
180
181 // Check for login or nicename.
182 $user = new \WP_User_Query(
183 array(
184 'count_total' => false,
185 'search' => $username,
186 'search_columns' => array( 'user_login', 'user_nicename' ),
187 'number' => 1,
188 'hide_empty' => true,
189 'fields' => 'ID',
190 )
191 );
192
193 if ( $user->get_results() ) {
194 return \current( $user->get_results() );
195 }
196
197 return new \WP_Error(
198 'activitypub_user_not_found',
199 \__( 'Actor not found', 'activitypub' ),
200 array( 'status' => 404 )
201 );
202 }
203
204 /**
205 * Get the Actor by resource URI (acct, http(s), etc).
206 *
207 * @param string $uri The actor resource URI.
208 *
209 * @return User|Blog|Application|\WP_Error Actor object or WP_Error if not found.
210 */
211 public static function get_by_resource( $uri ) {
212 $id = self::get_id_by_resource( $uri );
213 if ( \is_wp_error( $id ) ) {
214 return $id;
215 }
216
217 return self::get_by_id( $id );
218 }
219
220 /**
221 * Get the Actor by resource URI (acct, http(s), etc).
222 *
223 * @param string $uri The actor resource URI.
224 *
225 * @return int|\WP_Error Actor id or WP_Error if not found.
226 */
227 public static function get_id_by_resource( $uri ) {
228 $uri = object_to_uri( $uri );
229
230 if ( ! $uri ) {
231 return new \WP_Error(
232 'activitypub_no_uri',
233 \__( 'No URI provided', 'activitypub' ),
234 array( 'status' => 404 )
235 );
236 }
237
238 $scheme = 'acct';
239 $match = array();
240 // Try to extract the scheme and the host.
241 if ( \preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $uri, $match ) ) {
242 // Extract the scheme.
243 $scheme = \esc_attr( $match[1] );
244 }
245
246 // @todo: handle old domain URIs here before we serve a new domain below when we shouldn't.
247 // Although maybe passing through to ::get_by_username() is enough?
248
249 switch ( $scheme ) {
250 // Check for http(s) URIs.
251 case 'http':
252 case 'https':
253 // Check for http(s)://blog.example.com/@username.
254 $resource_path = \wp_parse_url( $uri, PHP_URL_PATH );
255
256 if ( $resource_path ) {
257 $blog_path = \wp_parse_url( \home_url(), PHP_URL_PATH );
258
259 if ( $blog_path ) {
260 $resource_path = \str_replace( $blog_path, '', $resource_path );
261 }
262
263 $resource_path = \trim( $resource_path, '/' );
264
265 if ( \str_starts_with( $resource_path, '@' ) ) {
266 $identifier = \str_replace( '@', '', $resource_path );
267 $identifier = \trim( $identifier, '/' );
268
269 return self::get_id_by_username( $identifier );
270 }
271 }
272
273 // Check for http(s)://blog.example.com/author/username.
274 $user_id = url_to_authorid( $uri );
275
276 if ( \is_int( $user_id ) ) {
277 return $user_id;
278 }
279
280 // Check for http(s)://blog.example.com/.
281 $normalized_uri = normalize_url( $uri );
282
283 if (
284 normalize_url( \site_url() ) === $normalized_uri ||
285 normalize_url( \home_url() ) === $normalized_uri
286 ) {
287 return self::BLOG_USER_ID;
288 }
289
290 return new \WP_Error(
291 'activitypub_no_user_found',
292 \__( 'Actor not found', 'activitypub' ),
293 array( 'status' => 404 )
294 );
295 // Check for acct URIs.
296 case 'acct':
297 $uri = \str_replace( 'acct:', '', $uri );
298 $identifier = \substr( $uri, 0, \strrpos( $uri, '@' ) );
299 $host = normalize_host( \substr( \strrchr( $uri, '@' ), 1 ) );
300 $blog_host = normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) );
301
302 if ( $blog_host !== $host && normalize_host( \get_option( 'activitypub_old_host' ) ) !== $host ) {
303 return new \WP_Error(
304 'activitypub_wrong_host',
305 \__( 'Resource host does not match blog host', 'activitypub' ),
306 array( 'status' => 404 )
307 );
308 }
309
310 // Prepare wildcards https://github.com/mastodon/mastodon/issues/22213.
311 if ( \in_array( $identifier, array( '_', '*', '' ), true ) ) {
312 return self::BLOG_USER_ID;
313 }
314
315 return self::get_id_by_username( $identifier );
316 default:
317 return new \WP_Error(
318 'activitypub_wrong_scheme',
319 \__( 'Wrong scheme', 'activitypub' ),
320 array( 'status' => 404 )
321 );
322 }
323 }
324
325 /**
326 * Get the Actor by various identifier types (ID, URI, username, or email).
327 *
328 * @param string|int $id Actor identifier (user ID, URI, username, or email).
329 *
330 * @return User|Blog|Application|\WP_Error Actor object or WP_Error if not found.
331 */
332 public static function get_by_various( $id ) {
333 $id = self::get_id_by_various( $id );
334 if ( \is_wp_error( $id ) ) {
335 return $id;
336 }
337
338 return self::get_by_id( $id );
339 }
340
341 /**
342 * Get the Actor by various identifier types (ID, URI, username, or email).
343 *
344 * @param string|int $id Actor identifier (user ID, URI, username, or email).
345 *
346 * @return int|\WP_Error Actor id or WP_Error if not found.
347 */
348 public static function get_id_by_various( $id ) {
349 if ( \is_numeric( $id ) ) {
350 $id = (int) $id;
351 } elseif (
352 // Is URL.
353 \filter_var( $id, FILTER_VALIDATE_URL ) ||
354 // Is acct.
355 \str_starts_with( $id, 'acct:' ) ||
356 // Is email.
357 \filter_var( $id, FILTER_VALIDATE_EMAIL )
358 ) {
359 $id = self::get_id_by_resource( $id );
360 } else {
361 $id = self::get_id_by_username( $id );
362 }
363
364 return $id;
365 }
366
367 /**
368 * Get the collection of all local user actors.
369 *
370 * @return Actor[] Array of User actor objects.
371 */
372 public static function get_collection() {
373 if ( is_user_type_disabled( 'user' ) ) {
374 return array();
375 }
376
377 $users = \get_users(
378 array(
379 'capability__in' => array( 'activitypub' ),
380 )
381 );
382
383 $return = array();
384
385 foreach ( $users as $user ) {
386 $actor = User::from_wp_user( $user->ID );
387
388 if ( \is_wp_error( $actor ) ) {
389 continue;
390 }
391
392 $return[] = $actor;
393 }
394
395 return $return;
396 }
397
398 /**
399 * Search local actors by name or username.
400 *
401 * Backs the actor autocomplete endpoint. Matches the search term against the WordPress user's
402 * login, nicename, and display name; the Blog actor is included when its name or identifier
403 * matches. Disabled actor types are excluded, mirroring get_collection()/get_all().
404 *
405 * @since 9.1.0
406 *
407 * @param string $query The search term.
408 * @param int $number Optional. Maximum number of actors to return. Default 10.
409 *
410 * @return Actor[] The matching local actor objects.
411 */
412 public static function search( $query, $number = 10 ) {
413 $actors = array();
414
415 if ( ! is_user_type_disabled( 'blog' ) ) {
416 $blog = new Blog();
417 if ( false !== \stripos( $blog->get_name(), $query ) || false !== \stripos( (string) $blog->get_preferred_username(), $query ) ) {
418 $actors[] = $blog;
419 }
420 }
421
422 // Only query as many users as the Blog actor left room for, so a match is never fetched then trimmed.
423 $remaining = $number - \count( $actors );
424
425 if ( $remaining > 0 && ! is_user_type_disabled( 'user' ) ) {
426 $users = \get_users(
427 array(
428 'capability__in' => array( 'activitypub' ),
429 'search' => '*' . $query . '*',
430 'search_columns' => array( 'user_login', 'user_nicename', 'display_name' ),
431 'number' => $remaining,
432 )
433 );
434
435 foreach ( $users as $user ) {
436 $actor = User::from_wp_user( $user->ID );
437 if ( ! \is_wp_error( $actor ) ) {
438 $actors[] = $actor;
439 }
440 }
441 }
442
443 return $actors;
444 }
445
446 /**
447 * Get all active actors, including the Blog actor if enabled.
448 *
449 * @return int[] Array of User and Blog actor IDs.
450 */
451 public static function get_all_ids() {
452 $user_ids = array();
453
454 if ( ! is_user_type_disabled( 'user' ) ) {
455 $user_ids = \get_users(
456 array(
457 'fields' => 'ID',
458 'capability__in' => array( 'activitypub' ),
459 )
460 );
461 }
462
463 // Also include the blog actor if active.
464 if ( ! is_user_type_disabled( 'blog' ) ) {
465 $user_ids[] = self::BLOG_USER_ID;
466 }
467
468 return \array_map( 'intval', $user_ids );
469 }
470
471 /**
472 * Get all active actors, including the Blog actor if enabled.
473 *
474 * @return Actor[] Array of User and Blog actor objects.
475 */
476 public static function get_all() {
477 $user_ids = self::get_all_ids();
478
479 $actors = \array_map( array( self::class, 'get_by_id' ), $user_ids );
480
481 // Filter out any WP_Error instances.
482 return \array_filter(
483 $actors,
484 static function ( $actor ) {
485 return ! \is_wp_error( $actor );
486 }
487 );
488 }
489
490 /**
491 * Returns the actor type based on the user ID.
492 *
493 * @param int $user_id The user ID to check.
494 *
495 * @return string Actor type: 'user' or 'blog'.
496 */
497 public static function get_type_by_id( $user_id ) {
498 $user_id = (int) $user_id;
499
500 if ( self::BLOG_USER_ID === $user_id ) {
501 return 'blog';
502 }
503
504 return 'user';
505 }
506
507 /**
508 * Return the public key for a given actor.
509 *
510 * @param int $user_id The WordPress User ID.
511 * @param bool $force Deprecated. Keys are never rotated; new pairs are only generated when none is stored.
512 *
513 * @return string The public key.
514 */
515 public static function get_public_key( $user_id, $force = false ) {
516 if ( $force ) {
517 \_deprecated_argument( __METHOD__, '9.1.0', \esc_html__( 'Keys are never rotated; new pairs are only generated when none is stored.', 'activitypub' ) );
518 }
519
520 $key_pair = self::get_keypair( $user_id );
521
522 return $key_pair['public_key'];
523 }
524
525 /**
526 * Return the private key for a given actor.
527 *
528 * @param int $user_id The WordPress User ID.
529 * @param bool $force Deprecated. Keys are never rotated; new pairs are only generated when none is stored.
530 *
531 * @return string The private key.
532 */
533 public static function get_private_key( $user_id, $force = false ) {
534 if ( $force ) {
535 \_deprecated_argument( __METHOD__, '9.1.0', \esc_html__( 'Keys are never rotated; new pairs are only generated when none is stored.', 'activitypub' ) );
536 }
537
538 $key_pair = self::get_keypair( $user_id );
539
540 return $key_pair['private_key'];
541 }
542
543 /**
544 * Return the key pair for a given actor.
545 *
546 * @param int $user_id The WordPress User ID.
547 *
548 * @return array The key pair.
549 */
550 public static function get_keypair( $user_id ) {
551 return Signature::get_key_pair(
552 self::get_signature_options_key( $user_id ),
553 function () use ( $user_id ) {
554 return self::check_legacy_key_pair( $user_id );
555 }
556 );
557 }
558
559 /**
560 * Return the option key for a given user.
561 *
562 * @param int $user_id The WordPress User ID.
563 *
564 * @return string The option key.
565 */
566 protected static function get_signature_options_key( $user_id ) {
567 if ( $user_id > 0 ) {
568 $user = \get_userdata( $user_id );
569 // Sanitize username because it could include spaces and special chars.
570 $user_id = \sanitize_title( $user->user_login );
571 }
572
573 return 'activitypub_keypair_for_' . $user_id;
574 }
575
576 /**
577 * Check if there is a legacy key pair
578 *
579 * @param int $user_id The WordPress User ID.
580 *
581 * @return array|bool The key pair or false.
582 */
583 protected static function check_legacy_key_pair( $user_id ) {
584 switch ( $user_id ) {
585 case 0:
586 $public_key = \get_option( 'activitypub_blog_user_public_key' );
587 $private_key = \get_option( 'activitypub_blog_user_private_key' );
588 break;
589 default:
590 $public_key = \get_user_meta( $user_id, 'magic_sig_public_key', true );
591 $private_key = \get_user_meta( $user_id, 'magic_sig_private_key', true );
592 break;
593 }
594
595 if ( ! empty( $public_key ) && \is_string( $public_key ) && ! empty( $private_key ) && \is_string( $private_key ) ) {
596 return array(
597 'private_key' => $private_key,
598 'public_key' => $public_key,
599 );
600 }
601
602 return false;
603 }
604
605 /**
606 * Determine if social graph (followers and following) should be shown for a given user.
607 *
608 * @param int $user_id The user ID.
609 *
610 * @return bool True if social graph should be shown, false otherwise.
611 */
612 public static function show_social_graph( $user_id ) {
613 if ( self::BLOG_USER_ID === (int) $user_id ) {
614 return ! (bool) \get_option( 'activitypub_hide_social_graph' );
615 } else {
616 return ! (bool) \get_user_option( 'activitypub_hide_social_graph', $user_id );
617 }
618 }
619 }
620