PluginProbe
ActivityPub / 7.7.0
ActivityPub v7.7.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 +280 -110 1.3.07.7.0 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,39 +100,48 @@
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 145 $description = get_user_meta( $this->_id, 'description', true );
120 146 }
121 147 return \wpautop( \wp_kses( $description, 'default' ) );
@@ -121,11 +147,11 @@
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 156 return \esc_url( \get_author_posts_url( $this->_id ) );
131 157 }
@@ -130,21 +156,46 @@
130 156 return \esc_url( \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( \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() {
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( wp_get_attachment_url( $icon ) ),
195 + );
196 + }
197 +
147 198 $icon = \esc_url(
148 199 \get_avatar_url(
149 200 $this->_id,
150 201 array( 'size' => 120 )
@@ -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( $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,9 +289,9 @@
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 /**
221 297 * Returns the Featured-API-Endpoint.
@@ -222,52 +298,39 @@
222 298 *
223 299 * @return string The Featured-Endpoint.
224 300 */
225 301 public function get_featured() {
226 - return get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $this->get__id() ) );
302 + return get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $this->get__id() ) );
227 303 }
228 304
229 305 /**
306 + * Returns the Featured-Tags-API-Endpoint.
307 + *
308 + * @return string The Featured-Tags-Endpoint.
309 + */
310 + public function get_featured_tags() {
311 + return get_rest_url_by_path( sprintf( 'actors/%d/collections/tags', $this->get__id() ) );
312 + }
313 +
314 + /**
315 + * Returns the endpoints.
316 + *
317 + * @return string[]|null The endpoints.
318 + */
319 + public function get_endpoints() {
320 + return array(
321 + 'sharedInbox' => get_rest_url_by_path( 'inbox' ),
322 + );
323 + }
324 +
325 + /**
230 326 * Extend the User-Output with Attachments.
231 327 *
232 328 * @return array The extended User-Output.
233 329 */
234 330 public function get_attachment() {
235 - $array = array();
236 -
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 - ),
245 - );
246 -
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 - );
256 -
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 - }
268 -
269 - return $array;
331 + $extra_fields = Extra_Fields::get_actor_fields( $this->_id );
332 + return Extra_Fields::fields_to_attachments( $extra_fields );
270 333 }
271 334
272 335 /**
273 336 * Returns a user@domain type of identifier for the user.
@@ -273,28 +336,135 @@
273 336 * Returns a user@domain type of identifier for the user.
274 337 *
275 338 * @return string The Webfinger-Identifier.
276 339 */
277 - public function get_resource() {
340 + public function get_webfinger() {
278 341 return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
279 342 }
280 343
344 + /**
345 + * Returns the canonical URL.
346 + *
347 + * @return string The canonical URL.
348 + */
281 349 public function get_canonical_url() {
282 350 return $this->get_url();
283 351 }
284 352
353 + /**
354 + * Returns the streams.
355 + *
356 + * @return null The streams.
357 + */
285 358 public function get_streams() {
286 359 return null;
287 360 }
288 361
362 + /**
363 + * Returns the tag.
364 + *
365 + * @return array The tag.
366 + */
289 367 public function get_tag() {
290 368 return array();
291 369 }
292 370
371 + /**
372 + * Returns the indexable state.
373 + *
374 + * @return bool Whether the user is indexable.
375 + */
293 376 public function get_indexable() {
294 - if ( \get_option( 'blog_public', 1 ) ) {
377 + if ( is_blog_public() ) {
295 378 return true;
296 379 } else {
297 380 return false;
298 381 }
382 + }
383 +
384 + /**
385 + * Update the username.
386 + *
387 + * @param string $value The new value.
388 + * @return int|\WP_Error The updated user ID or \WP_Error on failure.
389 + */
390 + public function update_name( $value ) {
391 + $userdata = array(
392 + 'ID' => $this->_id,
393 + 'display_name' => $value,
394 + );
395 + return \wp_update_user( $userdata );
396 + }
397 +
398 + /**
399 + * Update the User description.
400 + *
401 + * @param string $value The new value.
402 + * @return bool True if the attribute was updated, false otherwise.
403 + */
404 + public function update_summary( $value ) {
405 + return \update_user_option( $this->_id, 'activitypub_description', $value );
406 + }
407 +
408 + /**
409 + * Update the User icon.
410 + *
411 + * @param int $value The new value. Should be an attachment ID.
412 + * @return bool True if the attribute was updated, false otherwise.
413 + */
414 + public function update_icon( $value ) {
415 + if ( ! wp_attachment_is_image( $value ) ) {
416 + return false;
417 + }
418 + return update_user_option( $this->_id, 'activitypub_icon', $value );
419 + }
420 +
421 + /**
422 + * Update the User-Header-Image.
423 + *
424 + * @param int $value The new value. Should be an attachment ID.
425 + * @return bool True if the attribute was updated, false otherwise.
426 + */
427 + public function update_header( $value ) {
428 + if ( ! wp_attachment_is_image( $value ) ) {
429 + return false;
430 + }
431 + return \update_user_option( $this->_id, 'activitypub_header_image', $value );
432 + }
433 +
434 + /**
435 + * Returns the website hosts allowed to credit this blog.
436 + *
437 + * @return string[]|null The attribution domains or null if not found.
438 + */
439 + public function get_attribution_domains() {
440 + return get_attribution_domains();
441 + }
442 +
443 + /**
444 + * Returns the alsoKnownAs.
445 + *
446 + * @return string[] The alsoKnownAs.
447 + */
448 + public function get_also_known_as() {
449 + $also_known_as = array(
450 + \add_query_arg( 'author', $this->_id, \home_url( '/' ) ),
451 + $this->get_url(),
452 + $this->get_alternate_url(),
453 + );
454 +
455 + $also_known_as = array_merge( $also_known_as, \get_user_option( 'activitypub_also_known_as', $this->_id ) ?: array() );
456 +
457 + return array_unique( $also_known_as );
458 + }
459 +
460 + /**
461 + * Returns the movedTo.
462 + *
463 + * @return string The movedTo.
464 + */
465 + public function get_moved_to() {
466 + $moved_to = \get_user_option( 'activitypub_moved_to', $this->_id );
467 +
468 + return $moved_to && $moved_to !== $this->get_id() ? $moved_to : null;
299 469 }
300 470 }