PluginProbe
ActivityPub / trunk
ActivityPub vtrunk
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 / model / class-user.php

class-user.php in ActivityPub trunk, at includes/model/class-user.php

543 lines 12.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * User model file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Model;
9
10 use Activitypub\Activity\Actor;
11 use Activitypub\Collection\Actors;
12 use Activitypub\Collection\Extra_Fields;
13
14 use function Activitypub\get_attribution_domains;
15 use function Activitypub\get_rest_url_by_path;
16 use function Activitypub\is_blog_public;
17 use function Activitypub\user_can_activitypub;
18
19 /**
20 * User class.
21 *
22 * @method int get__id() Gets the WordPress user ID.
23 */
24 class User extends Actor {
25 /**
26 * The local User-ID (WP_User).
27 *
28 * @var int
29 */
30 protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
31
32 /**
33 * Whether the User is discoverable.
34 *
35 * @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
36 *
37 * @context http://joinmastodon.org/ns#discoverable
38 *
39 * @var boolean
40 */
41 protected $discoverable = true;
42
43 /**
44 * The generator of the object.
45 *
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
48 *
49 * @var array
50 */
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 );
60
61 /**
62 * Constructor.
63 *
64 * @param int $user_id Optional. The WordPress user ID. Default null.
65 */
66 public function __construct( $user_id = null ) {
67 if ( $user_id ) {
68 $this->_id = $user_id;
69
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 /**
80 * The type of the object.
81 *
82 * @return string The type of the object.
83 */
84 public function get_type() {
85 return 'Person';
86 }
87
88 /**
89 * Generate a User object from a WP_User.
90 *
91 * @param int $user_id The user ID.
92 *
93 * @return \WP_Error|User The User object or \WP_Error if user not found.
94 */
95 public static function from_wp_user( $user_id ) {
96 if ( ! user_can_activitypub( $user_id ) ) {
97 return new \WP_Error(
98 'activitypub_user_not_found',
99 \__( 'User not found', 'activitypub' ),
100 array( 'status' => 404 )
101 );
102 }
103
104 return new static( $user_id );
105 }
106
107 /**
108 * Get the user ID.
109 *
110 * @return string The user ID.
111 */
112 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( '/' ) );
126 }
127
128 /**
129 * Get the Username.
130 *
131 * @return string The Username.
132 */
133 public function get_name() {
134 return \get_the_author_meta( 'display_name', $this->_id );
135 }
136
137 /**
138 * Get the User description.
139 *
140 * @return string The User description.
141 */
142 public function get_summary() {
143 $description = \get_user_option( 'activitypub_description', $this->_id );
144 if ( empty( $description ) ) {
145 $description = \get_user_meta( $this->_id, 'description', true );
146 }
147 return \wpautop( \wp_kses( $description, 'default' ) );
148 }
149
150 /**
151 * Get the User url.
152 *
153 * @return string The User url.
154 */
155 public function get_url() {
156 return \esc_url_raw( \get_author_posts_url( $this->_id ) );
157 }
158
159 /**
160 * Returns the User URL with @-Prefix for the username.
161 *
162 * @return string The User URL with @-Prefix for the username.
163 */
164 public function get_alternate_url() {
165 return \esc_url_raw( \trailingslashit( \get_home_url() ) . '@' . $this->get_preferred_username() );
166 }
167
168 /**
169 * Get the preferred username.
170 *
171 * @return string The preferred username.
172 */
173 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;
182 }
183
184 /**
185 * Get the User icon.
186 *
187 * @return string[] The User icon.
188 */
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_raw( \wp_get_attachment_url( $icon ) ),
195 );
196 }
197
198 $icon = \esc_url_raw(
199 \get_avatar_url(
200 $this->_id,
201 array( 'size' => 120 )
202 )
203 );
204
205 return array(
206 'type' => 'Image',
207 'url' => $icon,
208 );
209 }
210
211 /**
212 * Returns the header image.
213 *
214 * @return string[]|null The header image.
215 */
216 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 ) {
229 return array(
230 'type' => 'Image',
231 'url' => \esc_url_raw( $image_url ),
232 );
233 }
234
235 return null;
236 }
237
238 /**
239 * Returns the date the user was created.
240 *
241 * @return false|string The date the user was created.
242 */
243 public function get_published() {
244 return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) );
245 }
246
247 /**
248 * Returns the public key.
249 *
250 * @return string[] The public key.
251 */
252 public function get_public_key() {
253 return array(
254 'id' => $this->get_id() . '#main-key',
255 'owner' => $this->get_id(),
256 'publicKeyPem' => Actors::get_public_key( $this->get__id() ),
257 );
258 }
259
260 /**
261 * Returns the Inbox-API-Endpoint.
262 *
263 * @return string The Inbox-Endpoint.
264 */
265 public function get_inbox() {
266 return get_rest_url_by_path( \sprintf( 'actors/%d/inbox', $this->get__id() ) );
267 }
268
269 /**
270 * Returns the Outbox-API-Endpoint.
271 *
272 * @return string The Outbox-Endpoint.
273 */
274 public function get_outbox() {
275 return get_rest_url_by_path( \sprintf( 'actors/%d/outbox', $this->get__id() ) );
276 }
277
278 /**
279 * Returns the Followers-API-Endpoint.
280 *
281 * @return string The Followers-Endpoint.
282 */
283 public function get_followers() {
284 return get_rest_url_by_path( \sprintf( 'actors/%d/followers', $this->get__id() ) );
285 }
286
287 /**
288 * Returns the Following-API-Endpoint.
289 *
290 * @return string The Following-Endpoint.
291 */
292 public function get_following() {
293 return get_rest_url_by_path( \sprintf( 'actors/%d/following', $this->get__id() ) );
294 }
295
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 /**
308 * Returns the Featured-API-Endpoint.
309 *
310 * @return string The Featured-Endpoint.
311 */
312 public function get_featured() {
313 return get_rest_url_by_path( \sprintf( 'actors/%d/collections/featured', $this->get__id() ) );
314 }
315
316 /**
317 * Returns the Featured-Tags-API-Endpoint.
318 *
319 * @return string The Featured-Tags-Endpoint.
320 */
321 public function get_featured_tags() {
322 return get_rest_url_by_path( \sprintf( 'actors/%d/collections/tags', $this->get__id() ) );
323 }
324
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' ),
338 );
339
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 }
347
348 return $endpoints;
349 }
350
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 );
359 }
360
361 /**
362 * Returns a user@domain type of identifier for the user.
363 *
364 * @return string The Webfinger-Identifier.
365 */
366 public function get_webfinger() {
367 return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
368 }
369
370 /**
371 * Returns the canonical URL.
372 *
373 * @return string The canonical URL.
374 */
375 public function get_canonical_url() {
376 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 }
542 }
543