PluginProbe
ActivityPub / 9.3.1
ActivityPub v9.3.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
activitypub / includes / model / class-blog.php

class-blog.php in ActivityPub 9.3.1, at includes/model/class-blog.php

637 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Blog 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\esc_hashtag;
15 use function Activitypub\get_attribution_domains;
16 use function Activitypub\get_rest_url_by_path;
17 use function Activitypub\is_blog_public;
18 use function Activitypub\is_single_user;
19 use function Activitypub\site_icon;
20
21 /**
22 * Blog class.
23 *
24 * @method int get__id() Gets the internal user ID for the blog (always returns BLOG_USER_ID).
25 */
26 class Blog extends Actor {
27 /**
28 * The User-ID
29 *
30 * @var int
31 */
32 protected $_id = Actors::BLOG_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
33
34 /**
35 * The generator of the object.
36 *
37 * @see https://www.w3.org/TR/activitypub/#generator
38 * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/844e/fep-844e.md#discovery-through-an-actor
39 *
40 * @var array
41 */
42 protected $generator = array(
43 'type' => 'Application',
44 'implements' => array(
45 array(
46 'href' => 'https://datatracker.ietf.org/doc/html/rfc9421',
47 'name' => 'RFC-9421: HTTP Message Signatures',
48 ),
49 ),
50 );
51
52 /**
53 * Constructor.
54 */
55 public function __construct() {
56 /**
57 * Fires when a model actor is constructed.
58 *
59 * @param Blog $this The Blog model.
60 */
61 \do_action( 'activitypub_construct_model_actor', $this );
62 }
63
64 /**
65 * Whether the User manually approves followers.
66 *
67 * @return false
68 */
69 public function get_manually_approves_followers() {
70 return false;
71 }
72
73 /**
74 * Whether the User is discoverable.
75 *
76 * @return boolean
77 */
78 public function get_discoverable() {
79 return true;
80 }
81
82 /**
83 * Get the User ID.
84 *
85 * @return string The User ID.
86 */
87 public function get_id() {
88 $id = parent::get_id();
89
90 if ( $id ) {
91 return $id;
92 }
93
94 $permalink = \get_option( 'activitypub_use_permalink_as_id_for_blog', false );
95
96 if ( $permalink ) {
97 return \esc_url_raw( \home_url( '/@' . $this->get_preferred_username() ) );
98 }
99
100 return \add_query_arg( 'author', $this->_id, \home_url( '/' ) );
101 }
102
103 /**
104 * Get the type of the object.
105 *
106 * If relay mode is enabled, return "Service".
107 * If the Blog is in "single user" mode, return "Person" instead of "Group".
108 *
109 * @return string The type of the object.
110 */
111 public function get_type() {
112 if ( \get_option( 'activitypub_relay_mode', false ) ) {
113 return 'Service';
114 }
115
116 if ( is_single_user() ) {
117 return 'Person';
118 } else {
119 return 'Group';
120 }
121 }
122
123 /**
124 * Get the Username.
125 *
126 * @return string The Username.
127 */
128 public function get_name() {
129 return \wp_strip_all_tags(
130 \html_entity_decode(
131 \get_bloginfo( 'name' ),
132 \ENT_QUOTES,
133 'UTF-8'
134 )
135 );
136 }
137
138 /**
139 * Get the User description.
140 *
141 * @return string The User description.
142 */
143 public function get_summary() {
144 $summary = \get_option( 'activitypub_blog_description', null );
145
146 if ( ! $summary ) {
147 $summary = \get_bloginfo( 'description' );
148 }
149
150 return \wpautop(
151 \wp_kses(
152 $summary,
153 'default'
154 )
155 );
156 }
157
158 /**
159 * Get the User url.
160 *
161 * @return string The User url.
162 */
163 public function get_url() {
164 return \get_bloginfo( 'url' );
165 }
166
167 /**
168 * Get blog's homepage URL.
169 *
170 * @return string The User-Url.
171 */
172 public function get_alternate_url() {
173 return \esc_url_raw( \trailingslashit( \get_home_url() ) );
174 }
175
176 /**
177 * Generate a default Username.
178 *
179 * @return string The auto-generated Username.
180 */
181 public static function get_default_username() {
182 // Check if domain host has a subdomain.
183 $host = \wp_parse_url( \get_home_url(), \PHP_URL_HOST );
184 $host = \preg_replace( '/^www\./i', '', $host );
185
186 /**
187 * Filters the default blog username.
188 *
189 * This filter allows developers to modify the default username that is
190 * generated for the blog, which by default is the site's host name
191 * without the 'www.' prefix.
192 *
193 * @param string $host The default username (site's host name).
194 */
195 return \apply_filters( 'activitypub_default_blog_username', $host );
196 }
197
198 /**
199 * Get the preferred Username.
200 *
201 * @return string The Username.
202 */
203 public function get_preferred_username() {
204 $username = \get_option( 'activitypub_blog_identifier' );
205
206 if ( $username ) {
207 return $username;
208 }
209
210 return self::get_default_username();
211 }
212
213 /**
214 * Get the User icon.
215 *
216 * @return string[] The User icon.
217 */
218 public function get_icon() {
219 return site_icon();
220 }
221
222 /**
223 * Get the User-Header-Image.
224 *
225 * @return string[]|null The User-Header-Image.
226 */
227 public function get_image() {
228 $header_image = \get_option( 'activitypub_header_image' );
229 $image_url = null;
230
231 if ( $header_image ) {
232 $image_url = \wp_get_attachment_url( $header_image );
233 }
234
235 if ( ! $image_url && \has_header_image() ) {
236 $image_url = \get_header_image();
237 }
238
239 if ( $image_url ) {
240 return array(
241 'type' => 'Image',
242 'url' => \esc_url_raw( $image_url ),
243 );
244 }
245
246 return null;
247 }
248
249 /**
250 * Get the published date.
251 *
252 * @return string The published date.
253 */
254 public function get_published() {
255 $published = \get_option( 'activitypub_blog_published' );
256
257 if ( $published ) {
258 return $published;
259 }
260
261 // Backfill from the first federated post.
262 $first_federated = new \WP_Query(
263 array(
264 'orderby' => 'date',
265 'order' => 'ASC',
266 'posts_per_page' => 1,
267 'post_status' => 'publish',
268 'no_found_rows' => true,
269 'ignore_sticky_posts' => true,
270 'update_post_meta_cache' => false,
271 'update_post_term_cache' => false,
272 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
273 'meta_query' => array(
274 array(
275 'key' => 'activitypub_status',
276 'compare' => 'EXISTS',
277 ),
278 ),
279 )
280 );
281
282 if ( ! empty( $first_federated->posts[0] ) ) {
283 $time = \strtotime( $first_federated->posts[0]->post_date_gmt );
284 } else {
285 $time = \time();
286 }
287
288 $published = \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $time );
289
290 \update_option( 'activitypub_blog_published', $published, false );
291
292 return $published;
293 }
294
295 /**
296 * Get the canonical URL.
297 *
298 * @return string|null The canonical URL.
299 */
300 public function get_canonical_url() {
301 return \home_url();
302 }
303
304 /**
305 * Get the Moderators endpoint.
306 *
307 * @return string|null The Moderators endpoint.
308 */
309 public function get_moderators() {
310 if ( is_single_user() || 'Group' !== $this->get_type() ) {
311 return null;
312 }
313
314 return get_rest_url_by_path( 'collections/moderators' );
315 }
316
317 /**
318 * Get attributedTo value.
319 *
320 * @return string|null The attributedTo value.
321 */
322 public function get_attributed_to() {
323 if ( is_single_user() || 'Group' !== $this->get_type() ) {
324 return null;
325 }
326
327 return get_rest_url_by_path( 'collections/moderators' );
328 }
329
330 /**
331 * Get the public key information.
332 *
333 * @return string[] The public key.
334 */
335 public function get_public_key() {
336 return array(
337 'id' => $this->get_id() . '#main-key',
338 'owner' => $this->get_id(),
339 'publicKeyPem' => Actors::get_public_key( $this->get__id() ),
340 );
341 }
342
343 /**
344 * Returns whether posting is restricted to mods.
345 *
346 * @return bool|null True if posting is restricted to mods, null if not applicable.
347 */
348 public function get_posting_restricted_to_mods() {
349 if ( 'Group' === $this->get_type() ) {
350 return true;
351 }
352
353 return null;
354 }
355
356 /**
357 * Returns the Inbox-API-Endpoint.
358 *
359 * @return string The Inbox-Endpoint.
360 */
361 public function get_inbox() {
362 return get_rest_url_by_path( \sprintf( 'actors/%d/inbox', $this->get__id() ) );
363 }
364
365 /**
366 * Returns the Outbox-API-Endpoint.
367 *
368 * @return string The Outbox-Endpoint.
369 */
370 public function get_outbox() {
371 return get_rest_url_by_path( \sprintf( 'actors/%d/outbox', $this->get__id() ) );
372 }
373
374 /**
375 * Returns the Followers-API-Endpoint.
376 *
377 * @return string The Followers-Endpoint.
378 */
379 public function get_followers() {
380 return get_rest_url_by_path( \sprintf( 'actors/%d/followers', $this->get__id() ) );
381 }
382
383 /**
384 * Returns the Following-API-Endpoint.
385 *
386 * @return string The Following-Endpoint.
387 */
388 public function get_following() {
389 return get_rest_url_by_path( \sprintf( 'actors/%d/following', $this->get__id() ) );
390 }
391
392 /**
393 * Returns endpoints.
394 *
395 * @return string[]|null The endpoints.
396 */
397 public function get_endpoints() {
398 $endpoints = array(
399 'sharedInbox' => get_rest_url_by_path( 'inbox' ),
400 'oauthAuthorizationEndpoint' => get_rest_url_by_path( 'oauth/authorize' ),
401 'oauthTokenEndpoint' => get_rest_url_by_path( 'oauth/token' ),
402 'oauthRegistrationEndpoint' => get_rest_url_by_path( 'oauth/clients' ),
403 'proxyUrl' => get_rest_url_by_path( 'proxy' ),
404 'proxyEventStream' => get_rest_url_by_path( 'proxy/stream' ),
405 );
406
407 if ( \get_option( 'activitypub_api', false ) ) {
408 /*
409 * RFC 6570 template. add_query_arg() picks the ?/& separator (plain permalinks already
410 * carry a query string) and does not encode values, so the {q} placeholder stays intact.
411 */
412 $endpoints['actorAutocomplete'] = \add_query_arg( 'q', '{q}', get_rest_url_by_path( 'actors/autocomplete' ) );
413 }
414
415 return $endpoints;
416 }
417
418 /**
419 * Returns a user@domain type of identifier for the user.
420 *
421 * @return string The Webfinger-Identifier.
422 */
423 public function get_webfinger() {
424 return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
425 }
426
427 /**
428 * Returns the Liked API endpoint.
429 *
430 * @since 8.1.0
431 *
432 * @return string The Liked endpoint.
433 */
434 public function get_liked() {
435 return get_rest_url_by_path( \sprintf( 'actors/%d/liked', $this->get__id() ) );
436 }
437
438 /**
439 * Returns the Featured-API-Endpoint.
440 *
441 * @return string The Featured-Endpoint.
442 */
443 public function get_featured() {
444 return get_rest_url_by_path( \sprintf( 'actors/%d/collections/featured', $this->get__id() ) );
445 }
446
447 /**
448 * Returns the Featured-Tags-API-Endpoint.
449 *
450 * @return string The Featured-Tags-Endpoint.
451 */
452 public function get_featured_tags() {
453 return get_rest_url_by_path( \sprintf( 'actors/%d/collections/tags', $this->get__id() ) );
454 }
455
456 /**
457 * Returns whether the site is indexable.
458 *
459 * @return bool Whether the site is indexable.
460 */
461 public function get_indexable() {
462 if ( is_blog_public() ) {
463 return true;
464 } else {
465 return false;
466 }
467 }
468
469 /**
470 * Update the Username.
471 *
472 * @param mixed $value The new value.
473 * @return bool True if the attribute was updated, false otherwise.
474 */
475 public function update_name( $value ) {
476 return \update_option( 'blogname', $value );
477 }
478
479 /**
480 * Update the User description.
481 *
482 * @param mixed $value The new value.
483 * @return bool True if the attribute was updated, false otherwise.
484 */
485 public function update_summary( $value ) {
486 return \update_option( 'blogdescription', $value );
487 }
488
489 /**
490 * Update the User icon.
491 *
492 * @param mixed $value The new value.
493 * @return bool True if the attribute was updated, false otherwise.
494 */
495 public function update_icon( $value ) {
496 if ( ! \wp_attachment_is_image( $value ) ) {
497 return false;
498 }
499 return \update_option( 'site_icon', $value );
500 }
501
502 /**
503 * Update the User-Header-Image.
504 *
505 * @param mixed $value The new value.
506 * @return bool True if the attribute was updated, false otherwise.
507 */
508 public function update_header( $value ) {
509 if ( ! \wp_attachment_is_image( $value ) ) {
510 return false;
511 }
512 return \update_option( 'activitypub_header_image', $value );
513 }
514
515 /**
516 * Get the User - Hashtags.
517 *
518 * @see https://docs.joinmastodon.org/spec/activitypub/#Hashtag
519 *
520 * @return string[] The User - Hashtags.
521 */
522 public function get_tag() {
523 $hashtags = array();
524
525 $args = array(
526 'orderby' => 'count',
527 'order' => 'DESC',
528 'number' => 10,
529 );
530
531 $tags = \get_tags( $args );
532
533 foreach ( $tags as $tag ) {
534 $hashtags[] = array(
535 'type' => 'Hashtag',
536 'href' => \get_tag_link( $tag->term_id ),
537 'name' => esc_hashtag( $tag->name ),
538 );
539 }
540
541 return $hashtags;
542 }
543
544 /**
545 * Extend the User-Output with Attachments.
546 *
547 * @return array The extended User-Output.
548 */
549 public function get_attachment() {
550 $extra_fields = Extra_Fields::get_actor_fields( $this->_id );
551 return Extra_Fields::fields_to_attachments( $extra_fields );
552 }
553
554 /**
555 * Returns the website hosts allowed to credit this blog.
556 *
557 * @return string[]|null The attribution domains or null if not found.
558 */
559 public function get_attribution_domains() {
560 return get_attribution_domains();
561 }
562
563 /**
564 * Returns the alsoKnownAs.
565 *
566 * @return string[] The alsoKnownAs.
567 */
568 public function get_also_known_as() {
569 $also_known_as = array(
570 \add_query_arg( 'author', $this->_id, \home_url( '/' ) ),
571 $this->get_url(),
572 $this->get_alternate_url(),
573 );
574
575 $also_known_as = \array_merge( $also_known_as, \get_option( 'activitypub_blog_user_also_known_as', array() ) );
576
577 return \array_unique( $also_known_as );
578 }
579
580 /**
581 * Returns the movedTo.
582 *
583 * @return string The movedTo.
584 */
585 public function get_moved_to() {
586 $moved_to = \get_option( 'activitypub_blog_user_moved_to' );
587
588 return $moved_to && $moved_to !== $this->get_id() ? $moved_to : null;
589 }
590
591 /**
592 * Get the actor-level interaction policy.
593 *
594 * Overrides the magic property accessor on Base_Object so that we always
595 * compute the policy from the current site setting rather than returning a
596 * cached property value. Currently only emits `canFeature` (FEP-7aa9).
597 * Driven by the site option `activitypub_default_feature_policy` and
598 * defaults to denying all featured-collection requests, in line with
599 * FEP-7aa9's "absence of policy = no consent" rule.
600 *
601 * @see https://w3id.org/fep/7aa9
602 *
603 * @since 9.0.0
604 *
605 * @return array
606 */
607 public function get_interaction_policy() {
608 $policy = array( 'canFeature' => $this->build_can_feature_policy() );
609
610 // Merge with an explicitly set interaction policy, if any.
611 if ( $this->interaction_policy ) {
612 $policy = \array_merge( (array) $this->interaction_policy, $policy );
613 }
614
615 return $policy;
616 }
617
618 /**
619 * Build the `canFeature` policy array from the site option.
620 *
621 * @return array
622 */
623 protected function build_can_feature_policy() {
624 $policy = \get_option( 'activitypub_default_feature_policy', ACTIVITYPUB_INTERACTION_POLICY_ME );
625
626 switch ( $policy ) {
627 case ACTIVITYPUB_INTERACTION_POLICY_ANYONE:
628 return array( 'automaticApproval' => array( 'https://www.w3.org/ns/activitystreams#Public' ) );
629 case ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS:
630 return array( 'automaticApproval' => array( $this->get_followers() ) );
631 case ACTIVITYPUB_INTERACTION_POLICY_ME:
632 default:
633 return array( 'automaticApproval' => array( $this->get_id() ) );
634 }
635 }
636 }
637