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
activitypub / includes / transformer / class-post.php

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

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