PluginProbe
ActivityPub / 7.8.2
ActivityPub v7.8.2
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.8.2, at includes/transformer/class-post.php

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