PluginProbe
ActivityPub / 8.3.0
ActivityPub v8.3.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
activitypub / includes / transformer / class-post.php

class-post.php in ActivityPub 8.3.0, at includes/transformer/class-post.php

1,216 lines 32.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WordPress Post Transformer Class file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Transformer;
9
10 use Activitypub\Activity\Base_Object;
11 use Activitypub\Blocks;
12 use Activitypub\Collection\Actors;
13 use Activitypub\Collection\Interactions;
14 use Activitypub\Collection\Replies;
15 use Activitypub\Model\Blog;
16 use Activitypub\Shortcodes;
17
18 use function Activitypub\esc_hashtag;
19 use function Activitypub\generate_post_summary;
20 use function Activitypub\get_content_visibility;
21 use function Activitypub\get_content_warning;
22 use function Activitypub\get_enclosures;
23 use function Activitypub\get_rest_url_by_path;
24 use function Activitypub\is_single_user;
25 use function Activitypub\site_supports_blocks;
26
27 /**
28 * WordPress Post Transformer.
29 *
30 * The Post Transformer is responsible for transforming a WP_Post object into different other
31 * Object-Types.
32 *
33 * Currently supported are:
34 *
35 * - Activitypub\Activity\Base_Object
36 */
37 class Post extends Base {
38 /**
39 * The User as Actor Object.
40 *
41 * @var \Activitypub\Activity\Actor
42 */
43 private $actor_object = null;
44
45 /**
46 * The content.
47 *
48 * @var string|false False indicates not yet computed.
49 */
50 private $content = false;
51
52 /**
53 * The summary.
54 *
55 * @var string|null|false False indicates not yet computed.
56 */
57 private $summary = false;
58
59 /**
60 * The tags.
61 *
62 * @var array|false False indicates not yet computed.
63 */
64 private $tags = false;
65
66 /**
67 * The attachment.
68 *
69 * @var array|false False indicates not yet computed.
70 */
71 private $attachment = false;
72
73 /**
74 * The mentions.
75 *
76 * @var array|false False indicates not yet computed.
77 */
78 private $mentions = false;
79
80 /**
81 * The in_reply_to.
82 *
83 * @var string|array|null|false False indicates not yet computed.
84 */
85 private $in_reply_to = false;
86
87 /**
88 * Transforms the WP_Post object to an ActivityPub Object
89 *
90 * @return \Activitypub\Activity\Base_Object The ActivityPub Object
91 */
92 public function to_object() {
93 $post = $this->item;
94 $object = parent::to_object();
95
96 $content_warning = get_content_warning( $post );
97 if ( ! empty( $content_warning ) ) {
98 $object->set_sensitive( true );
99 $object->set_summary( $content_warning );
100 $object->set_summary_map( null );
101 $object->set_dcterms( array( 'subject' => $content_warning ) );
102 }
103
104 return $object;
105 }
106
107 /**
108 * Returns a Tombstone object for the post.
109 *
110 * @return Base_Object The Tombstone object.
111 */
112 public function to_tombstone() {
113 $object = new Base_Object();
114 $object->set_type( 'Tombstone' );
115 $object->set_id( $this->get_id() );
116 $object->set_former_type( $this->get_type() );
117 $object->set_published( $this->get_published() );
118 $object->set_updated( $this->get_updated() );
119
120 $deleted_at = \get_post_meta( $this->item->ID, 'activitypub_deleted_at', true );
121 if ( $deleted_at ) {
122 $object->set_deleted( \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $deleted_at ) );
123 }
124
125 return $object;
126 }
127
128 /**
129 * Get the content visibility.
130 *
131 * @return string The content visibility.
132 */
133 public function get_content_visibility() {
134 if ( ! $this->content_visibility ) {
135 return get_content_visibility( $this->item );
136 }
137
138 return $this->content_visibility;
139 }
140
141 /**
142 * Get the Interaction Policy.
143 *
144 * @see https://docs.gotosocial.org/en/latest/federation/interaction_policy/
145 *
146 * @return array The interaction policy.
147 */
148 public function get_interaction_policy() {
149 return array(
150 'canAnnounce' => $this->get_public_interaction_policy(),
151 'canLike' => $this->get_public_interaction_policy(),
152 'canQuote' => $this->get_quote_policy(),
153 'canReply' => $this->get_public_interaction_policy(),
154 );
155 }
156
157 /**
158 * Returns the User-Object of the Author of the Post.
159 *
160 * If `single_user` mode is enabled, the Blog-User is returned.
161 *
162 * @return \Activitypub\Activity\Actor The User-Object.
163 */
164 public function get_actor_object() {
165 if ( $this->actor_object ) {
166 return $this->actor_object;
167 }
168
169 $blog_user = new Blog();
170 $this->actor_object = $blog_user;
171
172 if ( is_single_user() ) {
173 return $blog_user;
174 }
175
176 $user = Actors::get_by_id( $this->item->post_author );
177
178 if ( $user && ! is_wp_error( $user ) ) {
179 $this->actor_object = $user;
180 return $user;
181 }
182
183 return $blog_user;
184 }
185
186 /**
187 * Returns the ID of the Post.
188 *
189 * @return string The Posts ID.
190 */
191 public function get_id() {
192 $last_legacy_id = (int) \get_option( 'activitypub_last_post_with_permalink_as_id', 0 );
193 $post_id = (int) $this->item->ID;
194
195 if ( $post_id > $last_legacy_id ) {
196 // Generate URI based on post ID.
197 return \add_query_arg( 'p', $post_id, \home_url( '/' ) );
198 }
199
200 return $this->get_url();
201 }
202
203 /**
204 * Returns the URL of the Post.
205 *
206 * @return string The Posts URL.
207 */
208 public function get_url() {
209 $post = $this->item;
210
211 switch ( \get_post_status( $post ) ) {
212 case 'trash':
213 $permalink = \get_post_meta( $post->ID, '_activitypub_canonical_url', true );
214 break;
215 case 'draft':
216 // Get_sample_permalink is in wp-admin, not always loaded.
217 if ( ! \function_exists( '\get_sample_permalink' ) ) {
218 require_once ABSPATH . 'wp-admin/includes/post.php';
219 }
220 $sample = \get_sample_permalink( $post->ID );
221 $permalink = \str_replace( array( '%pagename%', '%postname%' ), $sample[1], $sample[0] );
222 break;
223 default:
224 $permalink = \get_permalink( $post );
225 break;
226 }
227
228 return \esc_url( $permalink );
229 }
230
231 /**
232 * Returns the User-URL of the Author of the Post.
233 *
234 * If `single_user` mode is enabled, the URL of the Blog-User is returned.
235 *
236 * @return string The User-URL.
237 */
238 protected function get_attributed_to() {
239 return $this->get_actor_object()->get_id();
240 }
241
242 /**
243 * Returns the featured image as `Image`.
244 *
245 * @return array|null The Image or null if no image is available.
246 */
247 protected function get_image() {
248 $post_id = $this->item->ID;
249
250 // List post thumbnail first if this post has one.
251 if (
252 ! \function_exists( 'has_post_thumbnail' ) ||
253 ! \has_post_thumbnail( $post_id )
254 ) {
255 return null;
256 }
257
258 $id = \get_post_thumbnail_id( $post_id );
259 $image_size = 'large';
260
261 /**
262 * Filter the image URL returned for each post.
263 *
264 * @param array|false $thumbnail The image URL, or false if no image is available.
265 * @param int $id The attachment ID.
266 * @param string $image_size The image size to retrieve. Set to 'large' by default.
267 */
268 $thumbnail = apply_filters(
269 'activitypub_get_image',
270 $this->get_attachment_image_src( $id, $image_size ),
271 $id,
272 $image_size
273 );
274
275 if ( ! $thumbnail ) {
276 return null;
277 }
278
279 $mime_type = \get_post_mime_type( $id );
280
281 $image = array(
282 'type' => 'Image',
283 'url' => \esc_url( $thumbnail[0] ),
284 'mediaType' => \esc_attr( $mime_type ),
285 );
286
287 $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
288 if ( $alt ) {
289 $image['name'] = \html_entity_decode( \wp_strip_all_tags( $alt ), ENT_QUOTES, 'UTF-8' );
290 }
291
292 return $image;
293 }
294
295 /**
296 * Returns an Icon, based on the Featured Image with a fallback to the site-icon.
297 *
298 * @return array|null The Icon or null if no icon is available.
299 */
300 protected function get_icon() {
301 $post_id = $this->item->ID;
302
303 // List post thumbnail first if this post has one.
304 if ( \has_post_thumbnail( $post_id ) ) {
305 $id = \get_post_thumbnail_id( $post_id );
306 } else {
307 // Try site_logo, falling back to site_icon, first.
308 $id = get_option( 'site_icon' );
309 }
310
311 if ( ! $id ) {
312 return null;
313 }
314
315 $image_size = 'thumbnail';
316
317 /**
318 * Filter the image URL returned for each post.
319 *
320 * @param array|false $thumbnail The image URL, or false if no image is available.
321 * @param int $id The attachment ID.
322 * @param string $image_size The image size to retrieve. Set to 'large' by default.
323 */
324 $thumbnail = apply_filters(
325 'activitypub_get_image',
326 $this->get_attachment_image_src( $id, $image_size ),
327 $id,
328 $image_size
329 );
330
331 if ( ! $thumbnail ) {
332 return null;
333 }
334
335 $mime_type = \get_post_mime_type( $id );
336
337 $image = array(
338 'type' => 'Image',
339 'url' => \esc_url( $thumbnail[0] ),
340 'mediaType' => \esc_attr( $mime_type ),
341 );
342
343 $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
344 if ( $alt ) {
345 $image['name'] = \html_entity_decode( \wp_strip_all_tags( $alt ), ENT_QUOTES, 'UTF-8' );
346 }
347
348 return $image;
349 }
350
351 /**
352 * Generates all Media Attachments for a Post.
353 *
354 * @return array The Attachments.
355 */
356 protected function get_attachment() {
357 if ( false !== $this->attachment ) {
358 return $this->attachment;
359 }
360
361 /*
362 * Remove attachments from the Fediverse if a post was federated and then unpublished.
363 * Except in preview mode, where we want to show attachments.
364 */
365 if ( ! $this->is_preview() && 'publish' !== \get_post_status( $this->item ) ) {
366 $this->attachment = array();
367
368 return $this->attachment;
369 }
370
371 $max_media = \get_post_meta( $this->item->ID, 'activitypub_max_image_attachments', true );
372
373 if ( ! is_numeric( $max_media ) ) {
374 $max_media = \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS );
375 }
376
377 /**
378 * Filters the maximum number of media attachments allowed in a post.
379 *
380 * Despite the name suggesting only images, this filter controls the maximum number
381 * of all media attachments (images, audio, and video) that can be included in an
382 * ActivityPub post. The name is maintained for backwards compatibility.
383 *
384 * @param int $max_media Maximum number of media attachments. Default ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS.
385 */
386 $max_media = (int) \apply_filters( 'activitypub_max_image_attachments', $max_media );
387
388 if ( 0 === $max_media ) {
389 $this->attachment = array();
390
391 return $this->attachment;
392 }
393
394 $media = array(
395 'image' => array(),
396 'audio' => array(),
397 'video' => array(),
398 );
399 $id = $this->item->ID;
400
401 // List post thumbnail first if this post has one.
402 if ( \has_post_thumbnail( $id ) ) {
403 $media['image'][] = array( 'id' => \get_post_thumbnail_id( $id ) );
404 }
405
406 $media = $this->get_enclosures( $media );
407
408 if ( site_supports_blocks() && \has_blocks( $this->item->post_content ) ) {
409 $media = $this->get_block_attachments( $media, $max_media );
410 } else {
411 $media = $this->parse_html_images( $media, $max_media, $this->item->post_content );
412 }
413
414 $media = $this->filter_media_by_object_type( $media, \get_post_format( $this->item ), $this->item );
415
416 /**
417 * Filter the attachment IDs for a post.
418 *
419 * @param array $media The media array grouped by type.
420 * @param \WP_Post $item The post object.
421 *
422 * @return array The filtered attachment IDs.
423 */
424 $media = \apply_filters( 'activitypub_attachment_ids', $media, $this->item );
425
426 // Deduplicate and limit after filter to ensure plugins adding attachments don't cause duplicates.
427 $media = $this->filter_unique_attachments( $media );
428 $media = \array_slice( $media, 0, $max_media );
429
430 $attachments = \array_filter( \array_map( array( $this, 'transform_attachment' ), $media ) );
431
432 /**
433 * Filter the attachments for a post.
434 *
435 * @param array $attachments The attachments.
436 * @param \WP_Post $item The post object.
437 *
438 * @return array The filtered attachments.
439 */
440 $this->attachment = \apply_filters( 'activitypub_attachments', $attachments, $this->item );
441
442 return $this->attachment;
443 }
444
445 /**
446 * Returns the ActivityStreams 2.0 Object-Type for a Post based on the
447 * settings and the Post-Type.
448 *
449 * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
450 *
451 * @return string The Object-Type.
452 */
453 protected function get_type() {
454 $post_format_setting = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
455
456 if ( 'wordpress-post-format' !== $post_format_setting ) {
457 $object_type = \ucfirst( $post_format_setting );
458 } elseif ( ! \post_type_supports( $this->item->post_type, 'title' ) || ! $this->item->post_title ) {
459 $object_type = 'Note';
460 } elseif ( 'page' === \get_post_type( $this->item ) ) {
461 $object_type = 'Page';
462 } elseif ( ! \get_post_format( $this->item ) ) {
463 $object_type = 'Article';
464 } else {
465 $object_type = 'Note';
466 }
467
468 /**
469 * Filters the ActivityPub object type for a post.
470 *
471 * Allows downstream consumers to override the discriminator that
472 * decides whether a post federates as Note, Article, or Page.
473 * The filtered value propagates to all internal callers of
474 * get_type(), including former_type/tombstone handling,
475 * summary and title decisions, the content template, and the
476 * preview guard, not only the wire-format type property.
477 *
478 * @since 8.1.1
479 *
480 * @param string $object_type The computed ActivityPub object type.
481 * @param \WP_Post $post The WordPress post being transformed.
482 */
483 return \apply_filters( 'activitypub_post_object_type', $object_type, $this->item );
484 }
485
486 /**
487 * Returns the Audience for the Post.
488 *
489 * @return string|null The audience.
490 */
491 public function get_audience() {
492 $actor_mode = \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE );
493
494 if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE === $actor_mode ) {
495 $blog = new Blog();
496 return $blog->get_id();
497 }
498
499 return null;
500 }
501
502 /**
503 * Returns a list of Tags, used in the Post.
504 *
505 * This includes Hash-Tags and Mentions.
506 *
507 * @return array The list of Tags.
508 */
509 protected function get_tag() {
510 if ( false !== $this->tags ) {
511 return $this->tags;
512 }
513
514 $tags = parent::get_tag();
515
516 $post_tags = \get_the_tags( $this->item->ID );
517 if ( $post_tags ) {
518 foreach ( $post_tags as $post_tag ) {
519 // Tag can be empty.
520 if ( ! $post_tag ) {
521 continue;
522 }
523
524 $tags[] = array(
525 'type' => 'Hashtag',
526 'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ),
527 'name' => esc_hashtag( $post_tag->name ),
528 );
529 }
530 }
531
532 $this->tags = \array_unique( $tags, SORT_REGULAR );
533
534 return $this->tags;
535 }
536
537 /**
538 * Returns the summary for the ActivityPub Item.
539 *
540 * The summary will be generated based on the user settings and only if the
541 * object type is not set to `note`.
542 *
543 * @return string|null The summary or null if the object type is `note`.
544 */
545 protected function get_summary() {
546 if ( 'Note' === $this->get_type() ) {
547 return null;
548 }
549
550 if ( false !== $this->summary ) {
551 return $this->summary;
552 }
553
554 // Remove Teaser from unpublished posts.
555 if ( ! $this->is_preview() && 'publish' !== \get_post_status( $this->item ) ) {
556 $this->summary = \__( '(This post is being modified)', 'activitypub' );
557
558 return $this->summary;
559 }
560
561 $this->summary = generate_post_summary( $this->item );
562
563 return $this->summary;
564 }
565
566 /**
567 * Returns the title for the ActivityPub Item.
568 *
569 * The title will be generated based on the user settings and only if the
570 * object type is not set to `note`.
571 *
572 * @return string|null The title or null if the object type is `note`.
573 */
574 protected function get_name() {
575 if ( 'Note' === $this->get_type() ) {
576 return null;
577 }
578
579 $title = \get_the_title( $this->item->ID );
580
581 if ( ! $title ) {
582 return null;
583 }
584
585 return \wp_strip_all_tags(
586 \html_entity_decode(
587 $title
588 )
589 );
590 }
591
592 /**
593 * Returns the content for the ActivityPub Item.
594 *
595 * The content will be generated based on the user settings.
596 *
597 * @return string The content.
598 */
599 protected function get_content() {
600 if ( false !== $this->content ) {
601 return $this->content;
602 }
603
604 // Remove Content from unpublished posts.
605 if ( ! $this->is_preview() && 'publish' !== \get_post_status( $this->item ) ) {
606 $this->content = \__( '(This post is being modified)', 'activitypub' );
607
608 return $this->content;
609 }
610
611 global $post;
612
613 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
614 $post = $this->item;
615 $content = $this->get_post_content_template();
616
617 /**
618 * Provides an action hook so plugins can add their own hooks/filters before AP content is generated.
619 *
620 * Example: if a plugin adds a filter to `the_content` to add a button to the end of posts, it can also remove that filter here.
621 *
622 * @param \WP_Post $post The post object.
623 */
624 \do_action( 'activitypub_before_get_content', $post );
625
626 // It seems that shortcodes are only applied to published posts.
627 if ( is_preview() ) {
628 $post->post_status = 'publish';
629 }
630
631 // Register our shortcodes just in time.
632 Shortcodes::register();
633 // Fill in the shortcodes.
634 \setup_postdata( $post );
635 $content = \do_shortcode( $content );
636 \wp_reset_postdata();
637
638 // Don't need these anymore, should never appear in a post.
639 Shortcodes::unregister();
640
641 /**
642 * Filters the post content after it was transformed for ActivityPub.
643 *
644 * @param string $content The transformed post content.
645 * @param \WP_Post $post The post object being transformed.
646 */
647 $this->content = \apply_filters( 'activitypub_the_content', $content, $post );
648
649 return $this->content;
650 }
651
652 /**
653 * Generate HTML @ link for reply block.
654 *
655 * @deprecated 7.4.0 Use {@see Blocks::generate_reply_link()}.
656 *
657 * @param string $block_content The block content.
658 * @param array $block The block data.
659 *
660 * @return string The HTML @ link.
661 */
662 public function generate_reply_link( $block_content, $block ) {
663 _deprecated_function( __METHOD__, '7.4.0', 'Activitypub\Blocks::generate_reply_link' );
664
665 return Blocks::generate_reply_link( $block_content, $block );
666 }
667
668 /**
669 * Returns the in-reply-to URL of the post.
670 *
671 * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-inreplyto
672 *
673 * @return string|array|null The in-reply-to URL of the post.
674 */
675 protected function get_in_reply_to() {
676 if ( false !== $this->in_reply_to ) {
677 return $this->in_reply_to;
678 }
679
680 if ( ! site_supports_blocks() ) {
681 $this->in_reply_to = null;
682 return $this->in_reply_to;
683 }
684
685 $reply_urls = array();
686 $blocks = \parse_blocks( $this->item->post_content );
687
688 foreach ( $blocks as $block ) {
689 if ( 'activitypub/reply' === $block['blockName'] && isset( $block['attrs']['url'] ) ) {
690
691 // Check if the URL has been validated as ActivityPub. Default to true for backwards compatibility.
692 if ( $block['attrs']['isValidActivityPub'] ?? true ) {
693 $reply_urls[] = $block['attrs']['url'];
694 }
695 }
696 }
697
698 if ( empty( $reply_urls ) ) {
699 $this->in_reply_to = null;
700
701 return $this->in_reply_to;
702 }
703
704 if ( 1 === count( $reply_urls ) ) {
705 $this->in_reply_to = \current( $reply_urls );
706
707 return $this->in_reply_to;
708 }
709
710 $this->in_reply_to = \array_values( \array_unique( $reply_urls ) );
711
712 return $this->in_reply_to;
713 }
714
715 /**
716 * Returns the published date of the post.
717 *
718 * @return string The published date of the post.
719 */
720 protected function get_published() {
721 $published = \strtotime( $this->item->post_date_gmt );
722
723 return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $published );
724 }
725
726 /**
727 * Returns the updated date of the post.
728 *
729 * @return string|null The updated date of the post.
730 */
731 protected function get_updated() {
732 $published = \strtotime( $this->item->post_date_gmt );
733 $updated = \strtotime( $this->item->post_modified_gmt );
734
735 if ( $updated > $published ) {
736 return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $updated );
737 }
738
739 return null;
740 }
741
742 /**
743 * Returns the location of the post as a Place object.
744 *
745 * Uses WordPress Geodata post meta fields to build the location.
746 *
747 * @see https://codex.wordpress.org/Geodata
748 * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-location
749 *
750 * @return array|null The Place object or null if no public geodata is available.
751 */
752 protected function get_location() {
753 $post_id = $this->item->ID;
754 $meta = \get_post_meta( $post_id );
755
756 // If geo_public exists and is explicitly set to 0, don't share location.
757 if ( isset( $meta['geo_public'] ) && '0' === $meta['geo_public'][0] ) {
758 return null;
759 }
760
761 // Both latitude and longitude are required for a valid location.
762 // Use is_numeric() instead of empty() since 0 is a valid coordinate (Equator/Prime Meridian).
763 $has_latitude = isset( $meta['geo_latitude'][0] ) && is_numeric( $meta['geo_latitude'][0] );
764 $has_longitude = isset( $meta['geo_longitude'][0] ) && is_numeric( $meta['geo_longitude'][0] );
765
766 if ( ! $has_latitude || ! $has_longitude ) {
767 return null;
768 }
769
770 $place = array(
771 'type' => 'Place',
772 'latitude' => (float) $meta['geo_latitude'][0],
773 'longitude' => (float) $meta['geo_longitude'][0],
774 );
775
776 // Add the address/name if available.
777 if ( ! empty( $meta['geo_address'][0] ) ) {
778 $place['name'] = \sanitize_text_field( $meta['geo_address'][0] );
779 }
780
781 /**
782 * Filter the location Place object for a post.
783 *
784 * @param array $place The Place object.
785 * @param \WP_Post $post The post object.
786 * @param int $post_id The post ID.
787 *
788 * @return array|null The filtered Place object or null to disable location.
789 */
790 return \apply_filters( 'activitypub_post_location', $place, $this->item, $post_id );
791 }
792
793 /**
794 * Helper function to extract the @-Mentions from the post content.
795 *
796 * @return array The list of @-Mentions.
797 */
798 protected function get_mentions() {
799 if ( false !== $this->mentions ) {
800 return $this->mentions;
801 }
802
803 /**
804 * Filter the mentions in the post content.
805 *
806 * @param array $mentions The mentions.
807 * @param string $content The post content.
808 * @param \WP_Post $post The post object.
809 *
810 * @return array The filtered mentions.
811 */
812 $this->mentions = apply_filters(
813 'activitypub_extract_mentions',
814 array(),
815 $this->item->post_content . ' ' . $this->item->post_excerpt,
816 $this->item
817 );
818
819 return $this->mentions;
820 }
821
822 /**
823 * Transform Embed blocks to block level link.
824 *
825 * Remote servers will simply drop iframe elements, rendering incomplete content.
826 *
827 * @deprecated 7.4.0 Use {@see Blocks::revert_embed_links()}.
828 *
829 * @see https://www.w3.org/TR/activitypub/#security-sanitizing-content
830 * @see https://www.w3.org/wiki/ActivityPub/Primer/HTML
831 *
832 * @param string $block_content The block content (html).
833 * @param object $block The block object.
834 *
835 * @return string A block level link
836 */
837 public function revert_embed_links( $block_content, $block ) {
838 _deprecated_function( __METHOD__, '7.4.0', 'Activitypub\Blocks::revert_embed_links' );
839
840 return Blocks::revert_embed_links( $block_content, $block );
841 }
842
843 /**
844 * Check if the post is a preview.
845 *
846 * @return boolean True if the post is a preview, false otherwise.
847 */
848 private function is_preview() {
849 return defined( 'ACTIVITYPUB_PREVIEW' ) && ACTIVITYPUB_PREVIEW;
850 }
851
852 /**
853 * Get enclosures for a post.
854 *
855 * @param array $media The media array grouped by type.
856 *
857 * @return array The media array extended with enclosures.
858 */
859 protected function get_enclosures( $media ) {
860 $enclosures = get_enclosures( $this->item->ID );
861
862 if ( ! $enclosures ) {
863 return $media;
864 }
865
866 foreach ( $enclosures as $enclosure ) {
867 // Check if URL is an attachment.
868 $attachment_id = \attachment_url_to_postid( $enclosure['url'] );
869
870 if ( $attachment_id ) {
871 $enclosure['id'] = $attachment_id;
872 $enclosure['url'] = \wp_get_attachment_url( $attachment_id );
873 $enclosure['mediaType'] = \get_post_mime_type( $attachment_id );
874 }
875
876 $mime_type = $enclosure['mediaType'];
877 $media_type = \strtok( $mime_type, '/' );
878 $enclosure['type'] = \ucfirst( $media_type );
879
880 switch ( $media_type ) {
881 case 'image':
882 $media['image'][] = $enclosure;
883 break;
884 case 'audio':
885 $media['audio'][] = $enclosure;
886 break;
887 case 'video':
888 $media['video'][] = $enclosure;
889 break;
890 }
891 }
892
893 return $media;
894 }
895
896 /**
897 * Get media attachments from blocks. They will be formatted as ActivityPub attachments, not as WP attachments.
898 *
899 * @param array $media The media array grouped by type.
900 * @param int $max_media The maximum number of attachments to return.
901 *
902 * @return array The attachments.
903 */
904 protected function get_block_attachments( $media, $max_media ) {
905 // Max media can't be negative or zero.
906 if ( $max_media <= 0 ) {
907 return array();
908 }
909
910 $blocks = \parse_blocks( $this->item->post_content );
911
912 return $this->get_media_from_blocks( $blocks, $media );
913 }
914
915 /**
916 * Recursively get media IDs from blocks.
917 *
918 * @param array $blocks The blocks to search for media IDs.
919 * @param array $media The media IDs to append new IDs to.
920 *
921 * @return array The image IDs.
922 */
923 protected function get_media_from_blocks( $blocks, $media ) {
924 foreach ( $blocks as $block ) {
925 // Recurse into inner blocks.
926 if ( ! empty( $block['innerBlocks'] ) ) {
927 $media = $this->get_media_from_blocks( $block['innerBlocks'], $media );
928 }
929
930 switch ( $block['blockName'] ) {
931 case 'core/image':
932 case 'core/cover':
933 if ( ! empty( $block['attrs']['id'] ) ) {
934 $alt = '';
935 $processor = new \WP_HTML_Tag_Processor( $block['innerHTML'] );
936 if ( $processor->next_tag( array( 'tag_name' => 'img' ) ) ) {
937 $alt = $processor->get_attribute( 'alt' ) ?? '';
938 }
939
940 $found = false;
941 foreach ( $media['image'] as $i => $image ) {
942 if ( isset( $image['id'] ) && $image['id'] === $block['attrs']['id'] ) {
943 $media['image'][ $i ]['alt'] = $alt;
944 $found = true;
945 break;
946 }
947 }
948
949 if ( ! $found ) {
950 $media['image'][] = array(
951 'id' => $block['attrs']['id'],
952 'alt' => $alt,
953 );
954 }
955 }
956 break;
957 case 'core/audio':
958 if ( ! empty( $block['attrs']['id'] ) ) {
959 $media['audio'][] = array( 'id' => $block['attrs']['id'] );
960 }
961 break;
962 case 'core/video':
963 case 'videopress/video':
964 if ( ! empty( $block['attrs']['id'] ) ) {
965 $video = array( 'id' => $block['attrs']['id'] );
966
967 // The poster is stored as an HTML attribute on the <video> tag, not in block attrs.
968 $processor = new \WP_HTML_Tag_Processor( $block['innerHTML'] );
969 if ( $processor->next_tag( array( 'tag_name' => 'video' ) ) ) {
970 $poster = $processor->get_attribute( 'poster' );
971 if ( ! empty( $poster ) ) {
972 $video['icon'] = \esc_url_raw( $poster );
973 }
974 }
975
976 $media['video'][] = $video;
977 }
978 break;
979 case 'jetpack/slideshow':
980 case 'jetpack/tiled-gallery':
981 if ( ! empty( $block['attrs']['ids'] ) ) {
982 $media['image'] = array_merge(
983 $media['image'],
984 array_map(
985 static function ( $id ) {
986 return array( 'id' => $id );
987 },
988 $block['attrs']['ids']
989 )
990 );
991 }
992 break;
993 case 'jetpack/image-compare':
994 if ( ! empty( $block['attrs']['beforeImageId'] ) ) {
995 $media['image'][] = array( 'id' => $block['attrs']['beforeImageId'] );
996 }
997 if ( ! empty( $block['attrs']['afterImageId'] ) ) {
998 $media['image'][] = array( 'id' => $block['attrs']['afterImageId'] );
999 }
1000 break;
1001 }
1002 }
1003
1004 return $media;
1005 }
1006
1007 /**
1008 * Filter media IDs by object type.
1009 *
1010 * @param array $media The media array grouped by type.
1011 * @param string $type The object type.
1012 * @param \WP_Post $item The post object.
1013 *
1014 * @return array The filtered media IDs.
1015 */
1016 protected function filter_media_by_object_type( $media, $type, $item ) {
1017 /**
1018 * Filter the object type for media attachments.
1019 *
1020 * @param string $type The object type.
1021 * @param \WP_Post $item The post object.
1022 *
1023 * @return string The filtered object type.
1024 */
1025 $type = \apply_filters( 'filter_media_by_object_type', \strtolower( $type ), $item );
1026
1027 if ( ! empty( $media[ $type ] ) ) {
1028 return $media[ $type ];
1029 }
1030
1031 return array_filter( array_merge( ...array_values( $media ) ) );
1032 }
1033
1034 /**
1035 * Converts a WordPress Attachment to an ActivityPub Attachment.
1036 *
1037 * @deprecated 7.2.0 Use {@see Base::transform_attachment()} instead.
1038 *
1039 * @param array $media The Attachment array.
1040 *
1041 * @return array The ActivityPub Attachment.
1042 */
1043 public function wp_attachment_to_activity_attachment( $media ) {
1044 _deprecated_function( __METHOD__, '7.2.0', '\Activitypub\Transformer\Base::transform_attachment()' );
1045
1046 return parent::transform_attachment( $media );
1047 }
1048
1049 /**
1050 * Get the context of the post.
1051 *
1052 * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-context
1053 *
1054 * @return string The context of the post.
1055 */
1056 protected function get_context() {
1057 return get_rest_url_by_path( sprintf( 'posts/%d/context', $this->item->ID ) );
1058 }
1059
1060 /**
1061 * Gets the template to use to generate the content of the activitypub item.
1062 *
1063 * @return string The Template.
1064 */
1065 protected function get_post_content_template() {
1066 $content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
1067 $template = $content ?: ACTIVITYPUB_CUSTOM_POST_CONTENT;
1068
1069 $post_format_setting = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
1070 $type = $this->get_type();
1071
1072 if ( 'wordpress-post-format' === $post_format_setting ) {
1073 $template = '';
1074
1075 /*
1076 * If the post is a note, not a reply, and does not have mentions
1077 * force the inclusion of the post title.
1078 */
1079 if (
1080 'Note' === $type
1081 && empty( $this->get_in_reply_to() )
1082 && empty( $this->get_mentions() )
1083 ) {
1084 $template .= '[ap_title type="html"]';
1085 }
1086
1087 $template .= '[ap_content]';
1088 }
1089
1090 /**
1091 * Filters the template used to generate ActivityPub object content.
1092 *
1093 * This filter allows developers to modify the template that determines how post
1094 * content is formatted in ActivityPub objects. The template can include special
1095 * shortcodes like [ap_title] and [ap_content] that are processed during content
1096 * generation.
1097 *
1098 * @since 7.6.0 Added the $type parameter.
1099 *
1100 * @param string $template The template string containing shortcodes.
1101 * @param \WP_Post $item The WordPress post object being transformed.
1102 * @param string $type ActivityStreams 2.0 Object-Type for the post.
1103 */
1104 return apply_filters( 'activitypub_object_content_template', $template, $this->item, $type );
1105 }
1106
1107 /**
1108 * Get the replies Collection.
1109 *
1110 * @return array|null The replies collection on success or null on failure.
1111 */
1112 public function get_replies() {
1113 return Replies::get_collection( $this->item );
1114 }
1115
1116 /**
1117 * Get the likes Collection.
1118 *
1119 * @return array The likes collection.
1120 */
1121 public function get_likes() {
1122 return array(
1123 'id' => get_rest_url_by_path( sprintf( 'posts/%d/likes', $this->item->ID ) ),
1124 'type' => 'Collection',
1125 'totalItems' => Interactions::count_by_type( $this->item->ID, 'like' ),
1126 );
1127 }
1128
1129 /**
1130 * Get the shares Collection.
1131 *
1132 * @return array The Shares collection.
1133 */
1134 public function get_shares() {
1135 return array(
1136 'id' => get_rest_url_by_path( sprintf( 'posts/%d/shares', $this->item->ID ) ),
1137 'type' => 'Collection',
1138 'totalItems' => Interactions::count_by_type( $this->item->ID, 'repost' ) + Interactions::count_by_type( $this->item->ID, 'quote' ),
1139 );
1140 }
1141
1142 /**
1143 * Get the preview of the post.
1144 *
1145 * @return array|null The preview of the post or null if the post is not an Article.
1146 */
1147 public function get_preview() {
1148 if ( 'Article' !== $this->get_type() ) {
1149 return null;
1150 }
1151
1152 return array(
1153 'type' => 'Note',
1154 'content' => $this->get_summary(),
1155 );
1156 }
1157
1158 /**
1159 * Get the quote policy.
1160 *
1161 * @return array The quote policy.
1162 */
1163 private function get_quote_policy() {
1164 $policy = \get_post_meta( $this->item->ID, 'activitypub_interaction_policy_quote', true );
1165
1166 // Fall back to global default if not set.
1167 if ( ! $policy ) {
1168 $policy = \get_option( 'activitypub_default_quote_policy', ACTIVITYPUB_INTERACTION_POLICY_ANYONE );
1169 }
1170
1171 switch ( $policy ) {
1172 case ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS:
1173 return array( 'automaticApproval' => get_rest_url_by_path( sprintf( 'actors/%d/followers', $this->item->post_author ) ) );
1174
1175 case ACTIVITYPUB_INTERACTION_POLICY_ME:
1176 return array( 'automaticApproval' => $this->get_self_interaction_policy() );
1177
1178 default:
1179 return $this->get_public_interaction_policy();
1180 }
1181 }
1182
1183 /**
1184 * Get the public interaction policy.
1185 *
1186 * @return array The public interaction policy.
1187 */
1188 private function get_public_interaction_policy() {
1189 return array(
1190 'automaticApproval' => 'https://www.w3.org/ns/activitystreams#Public',
1191 'always' => 'https://www.w3.org/ns/activitystreams#Public',
1192 );
1193 }
1194
1195 /**
1196 * Get the actor ID(s) for the `me` audience for use in interaction policies.
1197 *
1198 * @return string|array The actor ID(s).
1199 */
1200 private function get_self_interaction_policy() {
1201 switch ( \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
1202 case ACTIVITYPUB_BLOG_MODE:
1203 return ( new Blog() )->get_id();
1204
1205 case ACTIVITYPUB_ACTOR_AND_BLOG_MODE:
1206 return array(
1207 $this->get_actor_object()->get_id(),
1208 ( new Blog() )->get_id(),
1209 );
1210
1211 default:
1212 return $this->get_actor_object()->get_id();
1213 }
1214 }
1215 }
1216