PluginProbe
ActivityPub / 1.0.0
ActivityPub v1.0.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
← All changes | includes/model/class-user.php +96 -363 9.2.21.0.0 View file →
@@ -1,27 +1,16 @@
1 1 <?php
2 -/**
3 - * User model file.
4 - *
5 - * @package Activitypub
6 - */
7 -
8 2 namespace Activitypub\Model;
9 3
4 +use WP_Query;
5 +use WP_Error;
6 +use Activitypub\Signature;
7 +use Activitypub\Collection\Users;
10 8 use Activitypub\Activity\Actor;
11 -use Activitypub\Collection\Actors;
12 -use Activitypub\Collection\Extra_Fields;
13 9
14 -use function Activitypub\get_attribution_domains;
10 +use function Activitypub\is_user_disabled;
15 11 use function Activitypub\get_rest_url_by_path;
16 -use function Activitypub\is_blog_public;
17 -use function Activitypub\user_can_activitypub;
18 12
19 -/**
20 - * User class.
21 - *
22 - * @method int get__id() Gets the WordPress user ID.
23 - */
24 13 class User extends Actor {
25 14 /**
26 15 * The local User-ID (WP_User).
27 16 *
@@ -29,73 +18,49 @@
29 18 */
30 19 protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
31 20
32 21 /**
33 - * Whether the User is discoverable.
22 + * The Featured-Tags.
34 23 *
35 - * @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
24 + * @see https://docs.joinmastodon.org/spec/activitypub/#featuredTags
36 25 *
37 - * @context http://joinmastodon.org/ns#discoverable
38 - *
39 - * @var boolean
26 + * @var string
40 27 */
41 - protected $discoverable = true;
28 + protected $featured_tags;
42 29
43 30 /**
44 - * The generator of the object.
31 + * The Featured-Posts.
45 32 *
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
33 + * @see https://docs.joinmastodon.org/spec/activitypub/#featured
48 34 *
49 - * @var array
35 + * @var string
50 36 */
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 - );
37 + protected $featured;
60 38
61 39 /**
62 - * Constructor.
40 + * The User-Type
63 41 *
64 - * @param int $user_id Optional. The WordPress user ID. Default null.
42 + * @var string
65 43 */
66 - public function __construct( $user_id = null ) {
67 - if ( $user_id ) {
68 - $this->_id = $user_id;
44 + protected $type = 'Person';
69 45
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 - }
78 -
79 46 /**
80 - * The type of the object.
47 + * If the User is discoverable.
81 48 *
82 - * @return string The type of the object.
49 + * @var boolean
83 50 */
84 - public function get_type() {
85 - return 'Person';
86 - }
51 + protected $discoverable = true;
87 52
88 53 /**
89 - * Generate a User object from a WP_User.
54 + * The WebFinger Resource.
90 55 *
91 - * @param int $user_id The user ID.
92 - *
93 - * @return \WP_Error|User The User object or \WP_Error if user not found.
56 + * @var string<url>
94 57 */
58 + protected $resource;
59 +
95 60 public static function from_wp_user( $user_id ) {
96 - if ( ! user_can_activitypub( $user_id ) ) {
97 - return new \WP_Error(
61 + if ( is_user_disabled( $user_id ) ) {
62 + return new WP_Error(
98 63 'activitypub_user_not_found',
99 64 \__( 'User not found', 'activitypub' ),
100 65 array( 'status' => 404 )
101 66 );
@@ -100,103 +65,69 @@
100 65 array( 'status' => 404 )
101 66 );
102 67 }
103 68
104 - return new static( $user_id );
69 + $object = new static();
70 + $object->_id = $user_id;
71 +
72 + return $object;
105 73 }
106 74
107 75 /**
108 - * Get the user ID.
76 + * Get the User-ID.
109 77 *
110 - * @return string The user ID.
78 + * @return string The User-ID.
111 79 */
112 80 public function get_id() {
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( '/' ) );
81 + return $this->get_url();
126 82 }
127 83
128 84 /**
129 - * Get the Username.
85 + * Get the User-Name.
130 86 *
131 - * @return string The Username.
87 + * @return string The User-Name.
132 88 */
133 89 public function get_name() {
134 - return \get_the_author_meta( 'display_name', $this->_id );
90 + return \esc_attr( \get_the_author_meta( 'display_name', $this->_id ) );
135 91 }
136 92
137 93 /**
138 - * Get the User description.
94 + * Get the User-Description.
139 95 *
140 - * @return string The User description.
96 + * @return string The User-Description.
141 97 */
142 98 public function get_summary() {
143 - $description = \get_user_option( 'activitypub_description', $this->_id );
99 + $description = get_user_meta( $this->_id, 'activitypub_user_description', true );
144 100 if ( empty( $description ) ) {
145 - $description = \get_user_meta( $this->_id, 'description', true );
101 + $description = get_user_meta( $this->_id, 'description', true );
146 102 }
147 103 return \wpautop( \wp_kses( $description, 'default' ) );
148 104 }
149 105
150 106 /**
151 - * Get the User url.
107 + * Get the User-Url.
152 108 *
153 - * @return string The User url.
109 + * @return string The User-Url.
154 110 */
155 111 public function get_url() {
156 - return \esc_url_raw( \get_author_posts_url( $this->_id ) );
112 + return \esc_url( \get_author_posts_url( $this->_id ) );
157 113 }
158 114
159 115 /**
160 - * Returns the User URL with @-Prefix for the username.
116 + * Returns the User-URL with @-Prefix for the username.
161 117 *
162 - * @return string The User URL with @-Prefix for the username.
118 + * @return string The User-URL with @-Prefix for the username.
163 119 */
164 - public function get_alternate_url() {
165 - return \esc_url_raw( \trailingslashit( \get_home_url() ) . '@' . $this->get_preferred_username() );
120 + public function get_at_url() {
121 + return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() );
166 122 }
167 123
168 - /**
169 - * Get the preferred username.
170 - *
171 - * @return string The preferred username.
172 - */
173 124 public function get_preferred_username() {
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;
125 + return \esc_attr( \get_the_author_meta( 'login', $this->_id ) );
182 126 }
183 127
184 - /**
185 - * Get the User icon.
186 - *
187 - * @return string[] The User icon.
188 - */
189 128 public function get_icon() {
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(
129 + $icon = \esc_url(
199 130 \get_avatar_url(
200 131 $this->_id,
201 132 array( 'size' => 120 )
202 133 )
@@ -207,29 +138,14 @@
207 138 'url' => $icon,
208 139 );
209 140 }
210 141
211 - /**
212 - * Returns the header image.
213 - *
214 - * @return string[]|null The header image.
215 - */
216 142 public function get_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 ) {
143 + if ( \has_header_image() ) {
144 + $image = \esc_url( \get_header_image() );
229 145 return array(
230 146 'type' => 'Image',
231 - 'url' => \esc_url_raw( $image_url ),
147 + 'url' => $image,
232 148 );
233 149 }
234 150
235 151 return null;
@@ -234,27 +150,17 @@
234 150
235 151 return null;
236 152 }
237 153
238 - /**
239 - * Returns the date the user was created.
240 - *
241 - * @return false|string The date the user was created.
242 - */
243 154 public function get_published() {
244 - return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) );
155 + return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) );
245 156 }
246 157
247 - /**
248 - * Returns the public key.
249 - *
250 - * @return string[] The public key.
251 - */
252 158 public function get_public_key() {
253 159 return array(
254 - 'id' => $this->get_id() . '#main-key',
255 - 'owner' => $this->get_id(),
256 - 'publicKeyPem' => Actors::get_public_key( $this->get__id() ),
160 + 'id' => $this->get_id() . '#main-key',
161 + 'owner' => $this->get_id(),
162 + 'publicKeyPem' => Signature::get_public_key_for( $this->get__id() ),
257 163 );
258 164 }
259 165
260 166 /**
@@ -262,9 +168,9 @@
262 168 *
263 169 * @return string The Inbox-Endpoint.
264 170 */
265 171 public function get_inbox() {
266 - return get_rest_url_by_path( \sprintf( 'actors/%d/inbox', $this->get__id() ) );
172 + return get_rest_url_by_path( sprintf( 'users/%d/inbox', $this->get__id() ) );
267 173 }
268 174
269 175 /**
270 176 * Returns the Outbox-API-Endpoint.
@@ -271,9 +177,9 @@
271 177 *
272 178 * @return string The Outbox-Endpoint.
273 179 */
274 180 public function get_outbox() {
275 - return get_rest_url_by_path( \sprintf( 'actors/%d/outbox', $this->get__id() ) );
181 + return get_rest_url_by_path( sprintf( 'users/%d/outbox', $this->get__id() ) );
276 182 }
277 183
278 184 /**
279 185 * Returns the Followers-API-Endpoint.
@@ -280,9 +186,9 @@
280 186 *
281 187 * @return string The Followers-Endpoint.
282 188 */
283 189 public function get_followers() {
284 - return get_rest_url_by_path( \sprintf( 'actors/%d/followers', $this->get__id() ) );
190 + return get_rest_url_by_path( sprintf( 'users/%d/followers', $this->get__id() ) );
285 191 }
286 192
287 193 /**
288 194 * Returns the Following-API-Endpoint.
@@ -289,29 +195,18 @@
289 195 *
290 196 * @return string The Following-Endpoint.
291 197 */
292 198 public function get_following() {
293 - return get_rest_url_by_path( \sprintf( 'actors/%d/following', $this->get__id() ) );
199 + return get_rest_url_by_path( sprintf( 'users/%d/following', $this->get__id() ) );
294 200 }
295 201
296 202 /**
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 - /**
308 203 * Returns the Featured-API-Endpoint.
309 204 *
310 205 * @return string The Featured-Endpoint.
311 206 */
312 207 public function get_featured() {
313 - return get_rest_url_by_path( \sprintf( 'actors/%d/collections/featured', $this->get__id() ) );
208 + return get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $this->get__id() ) );
314 209 }
315 210
316 211 /**
317 212 * Returns the Featured-Tags-API-Endpoint.
@@ -318,225 +213,63 @@
318 213 *
319 214 * @return string The Featured-Tags-Endpoint.
320 215 */
321 216 public function get_featured_tags() {
322 - return get_rest_url_by_path( \sprintf( 'actors/%d/collections/tags', $this->get__id() ) );
217 + return get_rest_url_by_path( sprintf( 'users/%d/collections/tags', $this->get__id() ) );
323 218 }
324 219
325 220 /**
326 - * Returns the endpoints.
221 + * Extend the User-Output with Attachments.
327 222 *
328 - * @return string[]|null The endpoints.
223 + * @return array The extended User-Output.
329 224 */
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' ),
225 + public function get_attachment() {
226 + $array = array();
227 +
228 + $array[] = array(
229 + 'type' => 'PropertyValue',
230 + 'name' => \__( 'Blog', 'activitypub' ),
231 + 'value' => \html_entity_decode(
232 + '<a rel="me" title="' . \esc_attr( \home_url( '/' ) ) . '" target="_blank" href="' . \home_url( '/' ) . '">' . \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) . '</a>',
233 + \ENT_QUOTES,
234 + 'UTF-8'
235 + ),
338 236 );
339 237
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' ) );
238 + $array[] = array(
239 + 'type' => 'PropertyValue',
240 + 'name' => \__( 'Profile', 'activitypub' ),
241 + 'value' => \html_entity_decode(
242 + '<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>',
243 + \ENT_QUOTES,
244 + 'UTF-8'
245 + ),
246 + );
247 +
248 + if ( \get_the_author_meta( 'user_url', $this->get__id() ) ) {
249 + $array[] = array(
250 + 'type' => 'PropertyValue',
251 + 'name' => \__( 'Website', 'activitypub' ),
252 + 'value' => \html_entity_decode(
253 + '<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>',
254 + \ENT_QUOTES,
255 + 'UTF-8'
256 + ),
257 + );
346 258 }
347 259
348 - return $endpoints;
260 + return $array;
349 261 }
350 262
351 263 /**
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 );
359 - }
360 -
361 - /**
362 264 * Returns a user@domain type of identifier for the user.
363 265 *
364 266 * @return string The Webfinger-Identifier.
365 267 */
366 - public function get_webfinger() {
268 + public function get_resource() {
367 269 return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
368 270 }
369 271
370 - /**
371 - * Returns the canonical URL.
372 - *
373 - * @return string The canonical URL.
374 - */
375 272 public function get_canonical_url() {
376 273 return $this->get_url();
377 - }
378 -
379 - /**
380 - * Returns the streams.
381 - *
382 - * @return null The streams.
383 - */
384 - public function get_streams() {
385 - return null;
386 - }
387 -
388 - /**
389 - * Returns the tag.
390 - *
391 - * @return array The tag.
392 - */
393 - public function get_tag() {
394 - return array();
395 - }
396 -
397 - /**
398 - * Returns the indexable state.
399 - *
400 - * @return bool Whether the user is indexable.
401 - */
402 - public function get_indexable() {
403 - if ( is_blog_public() ) {
404 - return true;
405 - } else {
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() ) );
540 - }
541 274 }
542 275 }