PluginProbe
ActivityPub / 9.2.1
ActivityPub v9.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
← All changes | includes/model/class-user.php +353 -111 1.2.09.2.1 View file →
@@ -1,16 +1,27 @@
1 1 <?php
2 +/**
3 + * User model file.
4 + *
5 + * @package Activitypub
6 + */
7 +
2 8 namespace Activitypub\Model;
3 9
4 -use WP_Query;
5 -use WP_Error;
6 -use Activitypub\Signature;
7 -use Activitypub\Collection\Users;
8 10 use Activitypub\Activity\Actor;
11 +use Activitypub\Collection\Actors;
12 +use Activitypub\Collection\Extra_Fields;
9 13
10 -use function Activitypub\is_user_disabled;
14 +use function Activitypub\get_attribution_domains;
11 15 use function Activitypub\get_rest_url_by_path;
16 +use function Activitypub\is_blog_public;
17 +use function Activitypub\user_can_activitypub;
12 18
19 +/**
20 + * User class.
21 + *
22 + * @method int get__id() Gets the WordPress user ID.
23 + */
13 24 class User extends Actor {
14 25 /**
15 26 * The local User-ID (WP_User).
16 27 *
@@ -18,67 +29,73 @@
18 29 */
19 30 protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
20 31
21 32 /**
22 - * The Featured-Posts.
33 + * Whether the User is discoverable.
23 34 *
24 - * @see https://docs.joinmastodon.org/spec/activitypub/#featured
35 + * @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
25 36 *
26 - * @var string
37 + * @context http://joinmastodon.org/ns#discoverable
38 + *
39 + * @var boolean
27 40 */
28 - protected $featured;
41 + protected $discoverable = true;
29 42
30 43 /**
31 - * Moderators endpoint.
44 + * The generator of the object.
32 45 *
33 - * @see https://join-lemmy.org/docs/contributors/05-federation.html
46 + * @see https://www.w3.org/TR/activitypub/#generator
47 + * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/844e/fep-844e.md#discovery-through-an-actor
34 48 *
35 - * @var string
49 + * @var array
36 50 */
37 - protected $moderators;
51 + protected $generator = array(
52 + 'type' => 'Application',
53 + 'implements' => array(
54 + array(
55 + 'href' => 'https://datatracker.ietf.org/doc/html/rfc9421',
56 + 'name' => 'RFC-9421: HTTP Message Signatures',
57 + ),
58 + ),
59 + );
38 60
39 61 /**
40 - * The User-Type
62 + * Constructor.
41 63 *
42 - * @var string
64 + * @param int $user_id Optional. The WordPress user ID. Default null.
43 65 */
44 - protected $type = 'Person';
66 + public function __construct( $user_id = null ) {
67 + if ( $user_id ) {
68 + $this->_id = $user_id;
45 69
46 - /**
47 - * If the User is discoverable.
48 - *
49 - * @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
50 - *
51 - * @var boolean
52 - */
53 - protected $discoverable = true;
70 + /**
71 + * Fires when a model actor is constructed.
72 + *
73 + * @param User $this The User object.
74 + */
75 + \do_action( 'activitypub_construct_model_actor', $this );
76 + }
77 + }
54 78
55 79 /**
56 - * If the User is indexable.
80 + * The type of the object.
57 81 *
58 - * @var boolean
82 + * @return string The type of the object.
59 83 */
60 - protected $indexable;
84 + public function get_type() {
85 + return 'Person';
86 + }
61 87
62 88 /**
63 - * The WebFinger Resource.
89 + * Generate a User object from a WP_User.
64 90 *
65 - * @var string<url>
66 - */
67 - protected $resource;
68 -
69 - /**
70 - * Restrict posting to mods
91 + * @param int $user_id The user ID.
71 92 *
72 - * @see https://join-lemmy.org/docs/contributors/05-federation.html
73 - *
74 - * @var boolean
93 + * @return \WP_Error|User The User object or \WP_Error if user not found.
75 94 */
76 - protected $posting_restricted_to_mods = null;
77 -
78 95 public static function from_wp_user( $user_id ) {
79 - if ( is_user_disabled( $user_id ) ) {
80 - return new WP_Error(
96 + if ( ! user_can_activitypub( $user_id ) ) {
97 + return new \WP_Error(
81 98 'activitypub_user_not_found',
82 99 \__( 'User not found', 'activitypub' ),
83 100 array( 'status' => 404 )
84 101 );
@@ -83,69 +100,103 @@
83 100 array( 'status' => 404 )
84 101 );
85 102 }
86 103
87 - $object = new static();
88 - $object->_id = $user_id;
89 -
90 - return $object;
104 + return new static( $user_id );
91 105 }
92 106
93 107 /**
94 - * Get the User-ID.
108 + * Get the user ID.
95 109 *
96 - * @return string The User-ID.
110 + * @return string The user ID.
97 111 */
98 112 public function get_id() {
99 - return $this->get_url();
113 + $id = parent::get_id();
114 +
115 + if ( $id ) {
116 + return $id;
117 + }
118 +
119 + $permalink = \get_user_option( 'activitypub_use_permalink_as_id', $this->_id );
120 +
121 + if ( '1' === $permalink ) {
122 + return $this->get_url();
123 + }
124 +
125 + return \add_query_arg( 'author', $this->_id, \home_url( '/' ) );
100 126 }
101 127
102 128 /**
103 - * Get the User-Name.
129 + * Get the Username.
104 130 *
105 - * @return string The User-Name.
131 + * @return string The Username.
106 132 */
107 133 public function get_name() {
108 - return \esc_attr( \get_the_author_meta( 'display_name', $this->_id ) );
134 + return \get_the_author_meta( 'display_name', $this->_id );
109 135 }
110 136
111 137 /**
112 - * Get the User-Description.
138 + * Get the User description.
113 139 *
114 - * @return string The User-Description.
140 + * @return string The User description.
115 141 */
116 142 public function get_summary() {
117 - $description = get_user_meta( $this->_id, 'activitypub_user_description', true );
143 + $description = \get_user_option( 'activitypub_description', $this->_id );
118 144 if ( empty( $description ) ) {
119 - $description = get_user_meta( $this->_id, 'description', true );
145 + $description = \get_user_meta( $this->_id, 'description', true );
120 146 }
121 147 return \wpautop( \wp_kses( $description, 'default' ) );
122 148 }
123 149
124 150 /**
125 - * Get the User-Url.
151 + * Get the User url.
126 152 *
127 - * @return string The User-Url.
153 + * @return string The User url.
128 154 */
129 155 public function get_url() {
130 - return \esc_url( \get_author_posts_url( $this->_id ) );
156 + return \esc_url_raw( \get_author_posts_url( $this->_id ) );
131 157 }
132 158
133 159 /**
134 - * Returns the User-URL with @-Prefix for the username.
160 + * Returns the User URL with @-Prefix for the username.
135 161 *
136 - * @return string The User-URL with @-Prefix for the username.
162 + * @return string The User URL with @-Prefix for the username.
137 163 */
138 - public function get_at_url() {
139 - return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() );
164 + public function get_alternate_url() {
165 + return \esc_url_raw( \trailingslashit( \get_home_url() ) . '@' . $this->get_preferred_username() );
140 166 }
141 167
168 + /**
169 + * Get the preferred username.
170 + *
171 + * @return string The preferred username.
172 + */
142 173 public function get_preferred_username() {
143 - return \esc_attr( \get_the_author_meta( 'login', $this->_id ) );
174 + $login = \get_the_author_meta( 'login', $this->_id );
175 +
176 + // Handle cases where login is an email address (e.g., from Site Kit Google login).
177 + if ( \filter_var( $login, FILTER_VALIDATE_EMAIL ) ) {
178 + $login = \get_the_author_meta( 'user_nicename', $this->_id );
179 + }
180 +
181 + return $login;
144 182 }
145 183
184 + /**
185 + * Get the User icon.
186 + *
187 + * @return string[] The User icon.
188 + */
146 189 public function get_icon() {
147 - $icon = \esc_url(
190 + $icon = \get_user_option( 'activitypub_icon', $this->_id );
191 + if ( false !== $icon && \wp_attachment_is_image( $icon ) ) {
192 + return array(
193 + 'type' => 'Image',
194 + 'url' => \esc_url_raw( \wp_get_attachment_url( $icon ) ),
195 + );
196 + }
197 +
198 + $icon = \esc_url_raw(
148 199 \get_avatar_url(
149 200 $this->_id,
150 201 array( 'size' => 120 )
151 202 )
@@ -156,14 +207,29 @@
156 207 'url' => $icon,
157 208 );
158 209 }
159 210
211 + /**
212 + * Returns the header image.
213 + *
214 + * @return string[]|null The header image.
215 + */
160 216 public function get_image() {
161 - if ( \has_header_image() ) {
162 - $image = \esc_url( \get_header_image() );
217 + $header_image = \get_user_option( 'activitypub_header_image', $this->_id );
218 + $image_url = null;
219 +
220 + if ( ! $header_image && \has_header_image() ) {
221 + $image_url = \get_header_image();
222 + }
223 +
224 + if ( $header_image ) {
225 + $image_url = \wp_get_attachment_url( $header_image );
226 + }
227 +
228 + if ( $image_url ) {
163 229 return array(
164 230 'type' => 'Image',
165 - 'url' => $image,
231 + 'url' => \esc_url_raw( $image_url ),
166 232 );
167 233 }
168 234
169 235 return null;
@@ -168,17 +234,27 @@
168 234
169 235 return null;
170 236 }
171 237
238 + /**
239 + * Returns the date the user was created.
240 + *
241 + * @return false|string The date the user was created.
242 + */
172 243 public function get_published() {
173 - return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) );
244 + return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) );
174 245 }
175 246
247 + /**
248 + * Returns the public key.
249 + *
250 + * @return string[] The public key.
251 + */
176 252 public function get_public_key() {
177 253 return array(
178 - 'id' => $this->get_id() . '#main-key',
179 - 'owner' => $this->get_id(),
180 - 'publicKeyPem' => Signature::get_public_key_for( $this->get__id() ),
254 + 'id' => $this->get_id() . '#main-key',
255 + 'owner' => $this->get_id(),
256 + 'publicKeyPem' => Actors::get_public_key( $this->get__id() ),
181 257 );
182 258 }
183 259
184 260 /**
@@ -186,9 +262,9 @@
186 262 *
187 263 * @return string The Inbox-Endpoint.
188 264 */
189 265 public function get_inbox() {
190 - return get_rest_url_by_path( sprintf( 'users/%d/inbox', $this->get__id() ) );
266 + return get_rest_url_by_path( \sprintf( 'actors/%d/inbox', $this->get__id() ) );
191 267 }
192 268
193 269 /**
194 270 * Returns the Outbox-API-Endpoint.
@@ -195,9 +271,9 @@
195 271 *
196 272 * @return string The Outbox-Endpoint.
197 273 */
198 274 public function get_outbox() {
199 - return get_rest_url_by_path( sprintf( 'users/%d/outbox', $this->get__id() ) );
275 + return get_rest_url_by_path( \sprintf( 'actors/%d/outbox', $this->get__id() ) );
200 276 }
201 277
202 278 /**
203 279 * Returns the Followers-API-Endpoint.
@@ -204,9 +280,9 @@
204 280 *
205 281 * @return string The Followers-Endpoint.
206 282 */
207 283 public function get_followers() {
208 - return get_rest_url_by_path( sprintf( 'users/%d/followers', $this->get__id() ) );
284 + return get_rest_url_by_path( \sprintf( 'actors/%d/followers', $this->get__id() ) );
209 285 }
210 286
211 287 /**
212 288 * Returns the Following-API-Endpoint.
@@ -213,61 +289,74 @@
213 289 *
214 290 * @return string The Following-Endpoint.
215 291 */
216 292 public function get_following() {
217 - return get_rest_url_by_path( sprintf( 'users/%d/following', $this->get__id() ) );
293 + return get_rest_url_by_path( \sprintf( 'actors/%d/following', $this->get__id() ) );
218 294 }
219 295
220 296 /**
297 + * Returns the Liked API endpoint.
298 + *
299 + * @since 8.1.0
300 + *
301 + * @return string The Liked endpoint.
302 + */
303 + public function get_liked() {
304 + return get_rest_url_by_path( \sprintf( 'actors/%d/liked', $this->get__id() ) );
305 + }
306 +
307 + /**
221 308 * Returns the Featured-API-Endpoint.
222 309 *
223 310 * @return string The Featured-Endpoint.
224 311 */
225 312 public function get_featured() {
226 - return get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $this->get__id() ) );
313 + return get_rest_url_by_path( \sprintf( 'actors/%d/collections/featured', $this->get__id() ) );
227 314 }
228 315
229 316 /**
230 - * Extend the User-Output with Attachments.
317 + * Returns the Featured-Tags-API-Endpoint.
231 318 *
232 - * @return array The extended User-Output.
319 + * @return string The Featured-Tags-Endpoint.
233 320 */
234 - public function get_attachment() {
235 - $array = array();
321 + public function get_featured_tags() {
322 + return get_rest_url_by_path( \sprintf( 'actors/%d/collections/tags', $this->get__id() ) );
323 + }
236 324
237 - $array[] = array(
238 - 'type' => 'PropertyValue',
239 - 'name' => \__( 'Blog', 'activitypub' ),
240 - 'value' => \html_entity_decode(
241 - '<a rel="me" title="' . \esc_attr( \home_url( '/' ) ) . '" target="_blank" href="' . \home_url( '/' ) . '">' . \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) . '</a>',
242 - \ENT_QUOTES,
243 - 'UTF-8'
244 - ),
325 + /**
326 + * Returns the endpoints.
327 + *
328 + * @return string[]|null The endpoints.
329 + */
330 + public function get_endpoints() {
331 + $endpoints = array(
332 + 'sharedInbox' => get_rest_url_by_path( 'inbox' ),
333 + 'oauthAuthorizationEndpoint' => get_rest_url_by_path( 'oauth/authorize' ),
334 + 'oauthTokenEndpoint' => get_rest_url_by_path( 'oauth/token' ),
335 + 'oauthRegistrationEndpoint' => get_rest_url_by_path( 'oauth/clients' ),
336 + 'proxyUrl' => get_rest_url_by_path( 'proxy' ),
337 + 'proxyEventStream' => get_rest_url_by_path( 'proxy/stream' ),
245 338 );
246 339
247 - $array[] = array(
248 - 'type' => 'PropertyValue',
249 - 'name' => \__( 'Profile', 'activitypub' ),
250 - 'value' => \html_entity_decode(
251 - '<a rel="me" title="' . \esc_attr( \get_author_posts_url( $this->get__id() ) ) . '" target="_blank" href="' . \get_author_posts_url( $this->get__id() ) . '">' . \wp_parse_url( \get_author_posts_url( $this->get__id() ), \PHP_URL_HOST ) . '</a>',
252 - \ENT_QUOTES,
253 - 'UTF-8'
254 - ),
255 - );
340 + if ( \get_option( 'activitypub_api', false ) ) {
341 + /*
342 + * RFC 6570 template. add_query_arg() picks the ?/& separator (plain permalinks already
343 + * carry a query string) and does not encode values, so the {q} placeholder stays intact.
344 + */
345 + $endpoints['actorAutocomplete'] = \add_query_arg( 'q', '{q}', get_rest_url_by_path( 'actors/autocomplete' ) );
346 + }
256 347
257 - if ( \get_the_author_meta( 'user_url', $this->get__id() ) ) {
258 - $array[] = array(
259 - 'type' => 'PropertyValue',
260 - 'name' => \__( 'Website', 'activitypub' ),
261 - 'value' => \html_entity_decode(
262 - '<a rel="me" title="' . \esc_attr( \get_the_author_meta( 'user_url', $this->get__id() ) ) . '" target="_blank" href="' . \get_the_author_meta( 'user_url', $this->get__id() ) . '">' . \wp_parse_url( \get_the_author_meta( 'user_url', $this->get__id() ), \PHP_URL_HOST ) . '</a>',
263 - \ENT_QUOTES,
264 - 'UTF-8'
265 - ),
266 - );
267 - }
348 + return $endpoints;
349 + }
268 350
269 - return $array;
351 + /**
352 + * Extend the User-Output with Attachments.
353 + *
354 + * @return array The extended User-Output.
355 + */
356 + public function get_attachment() {
357 + $extra_fields = Extra_Fields::get_actor_fields( $this->_id );
358 + return Extra_Fields::fields_to_attachments( $extra_fields );
270 359 }
271 360
272 361 /**
273 362 * Returns a user@domain type of identifier for the user.
@@ -273,28 +362,181 @@
273 362 * Returns a user@domain type of identifier for the user.
274 363 *
275 364 * @return string The Webfinger-Identifier.
276 365 */
277 - public function get_resource() {
366 + public function get_webfinger() {
278 367 return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
279 368 }
280 369
370 + /**
371 + * Returns the canonical URL.
372 + *
373 + * @return string The canonical URL.
374 + */
281 375 public function get_canonical_url() {
282 376 return $this->get_url();
283 377 }
284 378
379 + /**
380 + * Returns the streams.
381 + *
382 + * @return null The streams.
383 + */
285 384 public function get_streams() {
286 385 return null;
287 386 }
288 387
388 + /**
389 + * Returns the tag.
390 + *
391 + * @return array The tag.
392 + */
289 393 public function get_tag() {
290 394 return array();
291 395 }
292 396
397 + /**
398 + * Returns the indexable state.
399 + *
400 + * @return bool Whether the user is indexable.
401 + */
293 402 public function get_indexable() {
294 - if ( \get_option( 'blog_public', 1 ) ) {
403 + if ( is_blog_public() ) {
295 404 return true;
296 405 } else {
297 406 return false;
407 + }
408 + }
409 +
410 + /**
411 + * Update the username.
412 + *
413 + * @param string $value The new value.
414 + * @return int|\WP_Error The updated user ID or \WP_Error on failure.
415 + */
416 + public function update_name( $value ) {
417 + $userdata = array(
418 + 'ID' => $this->_id,
419 + 'display_name' => $value,
420 + );
421 + return \wp_update_user( $userdata );
422 + }
423 +
424 + /**
425 + * Update the User description.
426 + *
427 + * @param string $value The new value.
428 + * @return bool True if the attribute was updated, false otherwise.
429 + */
430 + public function update_summary( $value ) {
431 + return \update_user_option( $this->_id, 'activitypub_description', $value );
432 + }
433 +
434 + /**
435 + * Update the User icon.
436 + *
437 + * @param int $value The new value. Should be an attachment ID.
438 + * @return bool True if the attribute was updated, false otherwise.
439 + */
440 + public function update_icon( $value ) {
441 + if ( ! \wp_attachment_is_image( $value ) ) {
442 + return false;
443 + }
444 + return \update_user_option( $this->_id, 'activitypub_icon', $value );
445 + }
446 +
447 + /**
448 + * Update the User-Header-Image.
449 + *
450 + * @param int $value The new value. Should be an attachment ID.
451 + * @return bool True if the attribute was updated, false otherwise.
452 + */
453 + public function update_header( $value ) {
454 + if ( ! \wp_attachment_is_image( $value ) ) {
455 + return false;
456 + }
457 + return \update_user_option( $this->_id, 'activitypub_header_image', $value );
458 + }
459 +
460 + /**
461 + * Returns the website hosts allowed to credit this blog.
462 + *
463 + * @return string[]|null The attribution domains or null if not found.
464 + */
465 + public function get_attribution_domains() {
466 + return get_attribution_domains();
467 + }
468 +
469 + /**
470 + * Returns the alsoKnownAs.
471 + *
472 + * @return string[] The alsoKnownAs.
473 + */
474 + public function get_also_known_as() {
475 + $also_known_as = array(
476 + \add_query_arg( 'author', $this->_id, \home_url( '/' ) ),
477 + $this->get_url(),
478 + $this->get_alternate_url(),
479 + );
480 +
481 + $also_known_as = \array_merge( $also_known_as, \get_user_option( 'activitypub_also_known_as', $this->_id ) ?: array() );
482 +
483 + return \array_unique( $also_known_as );
484 + }
485 +
486 + /**
487 + * Returns the movedTo.
488 + *
489 + * @return string The movedTo.
490 + */
491 + public function get_moved_to() {
492 + $moved_to = \get_user_option( 'activitypub_moved_to', $this->_id );
493 +
494 + return $moved_to && $moved_to !== $this->get_id() ? $moved_to : null;
495 + }
496 +
497 + /**
498 + * Get the actor-level interaction policy.
499 + *
500 + * Overrides the magic property accessor on Base_Object so that we always
501 + * compute the policy from the current site setting rather than returning a
502 + * cached property value. Currently only emits `canFeature` (FEP-7aa9).
503 + * Driven by the site option `activitypub_default_feature_policy` and
504 + * defaults to denying all featured-collection requests, in line with
505 + * FEP-7aa9's "absence of policy = no consent" rule.
506 + *
507 + * @see https://w3id.org/fep/7aa9
508 + *
509 + * @since 9.0.0
510 + *
511 + * @return array
512 + */
513 + public function get_interaction_policy() {
514 + $policy = array( 'canFeature' => $this->build_can_feature_policy() );
515 +
516 + // Merge with an explicitly set interaction policy, if any.
517 + if ( $this->interaction_policy ) {
518 + $policy = \array_merge( (array) $this->interaction_policy, $policy );
519 + }
520 +
521 + return $policy;
522 + }
523 +
524 + /**
525 + * Build the `canFeature` policy array from the site option.
526 + *
527 + * @return array
528 + */
529 + protected function build_can_feature_policy() {
530 + $policy = \get_option( 'activitypub_default_feature_policy', ACTIVITYPUB_INTERACTION_POLICY_ME );
531 +
532 + switch ( $policy ) {
533 + case ACTIVITYPUB_INTERACTION_POLICY_ANYONE:
534 + return array( 'automaticApproval' => array( 'https://www.w3.org/ns/activitystreams#Public' ) );
535 + case ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS:
536 + return array( 'automaticApproval' => array( $this->get_followers() ) );
537 + case ACTIVITYPUB_INTERACTION_POLICY_ME:
538 + default:
539 + return array( 'automaticApproval' => array( $this->get_id() ) );
298 540 }
299 541 }
300 542 }