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 +273 -125 2.1.17.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,73 +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 - * @context {
27 - * "@id": "http://joinmastodon.org/ns#featured",
28 - * "@type": "@id"
29 - * }
37 + * @context http://joinmastodon.org/ns#discoverable
30 38 *
31 - * @var string
39 + * @var boolean
32 40 */
33 - protected $featured;
41 + protected $discoverable = true;
34 42
35 43 /**
36 - * Moderators endpoint.
44 + * The generator of the object.
37 45 *
38 - * @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
39 48 *
40 - * @var string
49 + * @var array
41 50 */
42 - 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 + );
43 60
44 - public function get_type() {
45 - return 'Person';
46 - }
47 -
48 61 /**
49 - * If the User is discoverable.
62 + * Constructor.
50 63 *
51 - * @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
52 - *
53 - * @context http://joinmastodon.org/ns#discoverable
54 - *
55 - * @var boolean
64 + * @param int $user_id Optional. The WordPress user ID. Default null.
56 65 */
57 - protected $discoverable = true;
66 + public function __construct( $user_id = null ) {
67 + if ( $user_id ) {
68 + $this->_id = $user_id;
58 69
59 - /**
60 - * If the User is indexable.
61 - *
62 - * @context http://joinmastodon.org/ns#indexable
63 - *
64 - * @var boolean
65 - */
66 - protected $indexable;
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 + }
67 78
68 79 /**
69 - * The WebFinger Resource.
80 + * The type of the object.
70 81 *
71 - * @var string<url>
82 + * @return string The type of the object.
72 83 */
73 - protected $webfinger;
84 + public function get_type() {
85 + return 'Person';
86 + }
74 87
75 88 /**
76 - * Restrict posting to mods
89 + * Generate a User object from a WP_User.
77 90 *
78 - * @see https://join-lemmy.org/docs/contributors/05-federation.html
91 + * @param int $user_id The user ID.
79 92 *
80 - * @var boolean
93 + * @return \WP_Error|User The User object or \WP_Error if user not found.
81 94 */
82 - protected $posting_restricted_to_mods = null;
83 -
84 95 public static function from_wp_user( $user_id ) {
85 - if ( is_user_disabled( $user_id ) ) {
86 - return new WP_Error(
96 + if ( ! user_can_activitypub( $user_id ) ) {
97 + return new \WP_Error(
87 98 'activitypub_user_not_found',
88 99 \__( 'User not found', 'activitypub' ),
89 100 array( 'status' => 404 )
90 101 );
@@ -89,39 +100,48 @@
89 100 array( 'status' => 404 )
90 101 );
91 102 }
92 103
93 - $object = new static();
94 - $object->_id = $user_id;
95 -
96 - return $object;
104 + return new static( $user_id );
97 105 }
98 106
99 107 /**
100 - * Get the User-ID.
108 + * Get the user ID.
101 109 *
102 - * @return string The User-ID.
110 + * @return string The user ID.
103 111 */
104 112 public function get_id() {
105 - 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( '/' ) );
106 126 }
107 127
108 128 /**
109 - * Get the User-Name.
129 + * Get the Username.
110 130 *
111 - * @return string The User-Name.
131 + * @return string The Username.
112 132 */
113 133 public function get_name() {
114 - return \esc_attr( \get_the_author_meta( 'display_name', $this->_id ) );
134 + return \get_the_author_meta( 'display_name', $this->_id );
115 135 }
116 136
117 137 /**
118 - * Get the User-Description.
138 + * Get the User description.
119 139 *
120 - * @return string The User-Description.
140 + * @return string The User description.
121 141 */
122 142 public function get_summary() {
123 - $description = get_user_meta( $this->_id, 'activitypub_user_description', true );
143 + $description = get_user_option( 'activitypub_description', $this->_id );
124 144 if ( empty( $description ) ) {
125 145 $description = get_user_meta( $this->_id, 'description', true );
126 146 }
127 147 return \wpautop( \wp_kses( $description, 'default' ) );
@@ -127,11 +147,11 @@
127 147 return \wpautop( \wp_kses( $description, 'default' ) );
128 148 }
129 149
130 150 /**
131 - * Get the User-Url.
151 + * Get the User url.
132 152 *
133 - * @return string The User-Url.
153 + * @return string The User url.
134 154 */
135 155 public function get_url() {
136 156 return \esc_url( \get_author_posts_url( $this->_id ) );
137 157 }
@@ -136,21 +156,46 @@
136 156 return \esc_url( \get_author_posts_url( $this->_id ) );
137 157 }
138 158
139 159 /**
140 - * Returns the User-URL with @-Prefix for the username.
160 + * Returns the User URL with @-Prefix for the username.
141 161 *
142 - * @return string The User-URL with @-Prefix for the username.
162 + * @return string The User URL with @-Prefix for the username.
143 163 */
144 164 public function get_alternate_url() {
145 165 return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() );
146 166 }
147 167
168 + /**
169 + * Get the preferred username.
170 + *
171 + * @return string The preferred username.
172 + */
148 173 public function get_preferred_username() {
149 - 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;
150 182 }
151 183
184 + /**
185 + * Get the User icon.
186 + *
187 + * @return string[] The User icon.
188 + */
152 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 +
153 198 $icon = \esc_url(
154 199 \get_avatar_url(
155 200 $this->_id,
156 201 array( 'size' => 120 )
@@ -162,14 +207,29 @@
162 207 'url' => $icon,
163 208 );
164 209 }
165 210
211 + /**
212 + * Returns the header image.
213 + *
214 + * @return string[]|null The header image.
215 + */
166 216 public function get_image() {
167 - if ( \has_header_image() ) {
168 - $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 ) {
169 229 return array(
170 230 'type' => 'Image',
171 - 'url' => $image,
231 + 'url' => esc_url( $image_url ),
172 232 );
173 233 }
174 234
175 235 return null;
@@ -174,17 +234,27 @@
174 234
175 235 return null;
176 236 }
177 237
238 + /**
239 + * Returns the date the user was created.
240 + *
241 + * @return false|string The date the user was created.
242 + */
178 243 public function get_published() {
179 - 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 ) ) );
180 245 }
181 246
247 + /**
248 + * Returns the public key.
249 + *
250 + * @return string[] The public key.
251 + */
182 252 public function get_public_key() {
183 253 return array(
184 - 'id' => $this->get_id() . '#main-key',
185 - 'owner' => $this->get_id(),
186 - '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() ),
187 257 );
188 258 }
189 259
190 260 /**
@@ -192,9 +262,9 @@
192 262 *
193 263 * @return string The Inbox-Endpoint.
194 264 */
195 265 public function get_inbox() {
196 - 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() ) );
197 267 }
198 268
199 269 /**
200 270 * Returns the Outbox-API-Endpoint.
@@ -201,9 +271,9 @@
201 271 *
202 272 * @return string The Outbox-Endpoint.
203 273 */
204 274 public function get_outbox() {
205 - 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() ) );
206 276 }
207 277
208 278 /**
209 279 * Returns the Followers-API-Endpoint.
@@ -210,9 +280,9 @@
210 280 *
211 281 * @return string The Followers-Endpoint.
212 282 */
213 283 public function get_followers() {
214 - 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() ) );
215 285 }
216 286
217 287 /**
218 288 * Returns the Following-API-Endpoint.
@@ -219,9 +289,9 @@
219 289 *
220 290 * @return string The Following-Endpoint.
221 291 */
222 292 public function get_following() {
223 - 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() ) );
224 294 }
225 295
226 296 /**
227 297 * Returns the Featured-API-Endpoint.
@@ -228,21 +298,29 @@
228 298 *
229 299 * @return string The Featured-Endpoint.
230 300 */
231 301 public function get_featured() {
232 - 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() ) );
233 303 }
234 304
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 + */
235 319 public function get_endpoints() {
236 - $endpoints = null;
237 -
238 - if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) {
239 - $endpoints = array(
240 - 'sharedInbox' => get_rest_url_by_path( 'inbox' ),
241 - );
242 - }
243 -
244 - return $endpoints;
320 + return array(
321 + 'sharedInbox' => get_rest_url_by_path( 'inbox' ),
322 + );
245 323 }
246 324
247 325 /**
248 326 * Extend the User-Output with Attachments.
@@ -249,43 +327,10 @@
249 327 *
250 328 * @return array The extended User-Output.
251 329 */
252 330 public function get_attachment() {
253 - $array = array();
254 -
255 - $array[] = array(
256 - 'type' => 'PropertyValue',
257 - 'name' => \__( 'Blog', 'activitypub' ),
258 - 'value' => \html_entity_decode(
259 - '<a rel="me" title="' . \esc_attr( \home_url( '/' ) ) . '" target="_blank" href="' . \home_url( '/' ) . '">' . \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) . '</a>',
260 - \ENT_QUOTES,
261 - 'UTF-8'
262 - ),
263 - );
264 -
265 - $array[] = array(
266 - 'type' => 'PropertyValue',
267 - 'name' => \__( 'Profile', 'activitypub' ),
268 - 'value' => \html_entity_decode(
269 - '<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>',
270 - \ENT_QUOTES,
271 - 'UTF-8'
272 - ),
273 - );
274 -
275 - if ( \get_the_author_meta( 'user_url', $this->get__id() ) ) {
276 - $array[] = array(
277 - 'type' => 'PropertyValue',
278 - 'name' => \__( 'Website', 'activitypub' ),
279 - 'value' => \html_entity_decode(
280 - '<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>',
281 - \ENT_QUOTES,
282 - 'UTF-8'
283 - ),
284 - );
285 - }
286 -
287 - return $array;
331 + $extra_fields = Extra_Fields::get_actor_fields( $this->_id );
332 + return Extra_Fields::fields_to_attachments( $extra_fields );
288 333 }
289 334
290 335 /**
291 336 * Returns a user@domain type of identifier for the user.
@@ -295,28 +340,131 @@
295 340 public function get_webfinger() {
296 341 return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
297 342 }
298 343
299 - public function get_resource() {
300 - return $this->get_webfinger();
301 - }
302 -
344 + /**
345 + * Returns the canonical URL.
346 + *
347 + * @return string The canonical URL.
348 + */
303 349 public function get_canonical_url() {
304 350 return $this->get_url();
305 351 }
306 352
353 + /**
354 + * Returns the streams.
355 + *
356 + * @return null The streams.
357 + */
307 358 public function get_streams() {
308 359 return null;
309 360 }
310 361
362 + /**
363 + * Returns the tag.
364 + *
365 + * @return array The tag.
366 + */
311 367 public function get_tag() {
312 368 return array();
313 369 }
314 370
371 + /**
372 + * Returns the indexable state.
373 + *
374 + * @return bool Whether the user is indexable.
375 + */
315 376 public function get_indexable() {
316 - if ( \get_option( 'blog_public', 1 ) ) {
377 + if ( is_blog_public() ) {
317 378 return true;
318 379 } else {
319 380 return false;
320 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;
321 469 }
322 470 }