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

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

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