PluginProbe
ActivityPub / 3.2.4
ActivityPub v3.2.4
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 3.2.4, at includes/transformer/class-post.php

946 lines 25.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Activitypub\Transformer;
3
4 use WP_Post;
5 use Activitypub\Shortcodes;
6 use Activitypub\Model\Blog;
7 use Activitypub\Collection\Users;
8 use Activitypub\Transformer\Base;
9
10 use function Activitypub\esc_hashtag;
11 use function Activitypub\is_single_user;
12 use function Activitypub\get_enclosures;
13 use function Activitypub\site_supports_blocks;
14 use function Activitypub\get_rest_url_by_path;
15 use function Activitypub\is_user_type_disabled;
16 use function Activitypub\generate_post_summary;
17
18 /**
19 * WordPress Post Transformer
20 *
21 * The Post Transformer is responsible for transforming a WP_Post object into different other
22 * Object-Types.
23 *
24 * Currently supported are:
25 *
26 * - Activitypub\Activity\Base_Object
27 */
28 class Post extends Base {
29 /**
30 * The User as Actor Object.
31 *
32 * @var Activitypub\Activity\Actor
33 */
34 private $actor_object = null;
35
36 /**
37 * Returns the ID of the WordPress Post.
38 *
39 * @return int The ID of the WordPress Post
40 */
41 public function get_wp_user_id() {
42 return $this->wp_object->post_author;
43 }
44
45 /**
46 * Change the User-ID of the WordPress Post.
47 *
48 * @return int The User-ID of the WordPress Post
49 */
50 public function change_wp_user_id( $user_id ) {
51 $this->wp_object->post_author = $user_id;
52
53 return $this;
54 }
55
56 /**
57 * Transforms the WP_Post object to an ActivityPub Object
58 *
59 * @see \Activitypub\Activity\Base_Object
60 *
61 * @return \Activitypub\Activity\Base_Object The ActivityPub Object
62 */
63 public function to_object() {
64 $post = $this->wp_object;
65 $object = parent::to_object();
66
67 $published = \strtotime( $post->post_date_gmt );
68
69 $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) );
70
71 $updated = \strtotime( $post->post_modified_gmt );
72
73 if ( $updated > $published ) {
74 $object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) );
75 }
76
77 $object->set_content_map(
78 array(
79 $this->get_locale() => $this->get_content(),
80 )
81 );
82
83 $object->set_to(
84 array(
85 'https://www.w3.org/ns/activitystreams#Public',
86 $this->get_actor_object()->get_followers(),
87 )
88 );
89
90 return $object;
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 protected 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 = Users::get_by_id( $this->wp_object->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 return $this->get_url();
129 }
130
131 /**
132 * Returns the URL of the Post.
133 *
134 * @return string The Posts URL.
135 */
136 public function get_url() {
137 $post = $this->wp_object;
138
139 switch ( \get_post_status( $post ) ) {
140 case 'trash':
141 $permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true );
142 break;
143 case 'draft':
144 // get_sample_permalink is in wp-admin, not always loaded
145 if ( ! \function_exists( '\get_sample_permalink' ) ) {
146 require_once ABSPATH . 'wp-admin/includes/post.php';
147 }
148 $sample = \get_sample_permalink( $post->ID );
149 $permalink = \str_replace( array( '%pagename%', '%postname%' ), $sample[1], $sample[0] );
150 break;
151 default:
152 $permalink = \get_permalink( $post );
153 break;
154 }
155
156 return \esc_url( $permalink );
157 }
158
159 /**
160 * Returns the User-URL of the Author of the Post.
161 *
162 * If `single_user` mode is enabled, the URL of the Blog-User is returned.
163 *
164 * @return string The User-URL.
165 */
166 protected function get_attributed_to() {
167 return $this->get_actor_object()->get_url();
168 }
169
170 /**
171 * Generates all Media Attachments for a Post.
172 *
173 * @return array The Attachments.
174 */
175 protected function get_attachment() {
176 // Remove attachments from drafts.
177 if ( 'draft' === \get_post_status( $this->wp_object ) ) {
178 return array();
179 }
180
181 // Once upon a time we only supported images, but we now support audio/video as well.
182 // We maintain the image-centric naming for backwards compatibility.
183 $max_media = \intval(
184 \apply_filters(
185 'activitypub_max_image_attachments',
186 \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS )
187 )
188 );
189
190 $media = array(
191 'audio' => array(),
192 'video' => array(),
193 'image' => array(),
194 );
195 $id = $this->wp_object->ID;
196
197 // list post thumbnail first if this post has one
198 if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
199 $media['image'][] = array( 'id' => \get_post_thumbnail_id( $id ) );
200 }
201
202 $media = $this->get_enclosures( $media );
203
204 if ( site_supports_blocks() && \has_blocks( $this->wp_object->post_content ) ) {
205 $media = $this->get_block_attachments( $media, $max_media );
206 } else {
207 $media = $this->get_classic_editor_images( $media, $max_media );
208 }
209
210 $media = self::filter_media_by_object_type( $media, \get_post_format( $this->wp_object ), $this->wp_object );
211 $unique_ids = \array_unique( \array_column( $media, 'id' ) );
212 $media = \array_intersect_key( $media, $unique_ids );
213 $media = \array_slice( $media, 0, $max_media );
214
215 /**
216 * Filter the attachment IDs for a post.
217 *
218 * @param array $media The media array grouped by type.
219 * @param WP_Post $this->wp_object The post object.
220 *
221 * @return array The filtered attachment IDs.
222 */
223 $media = \apply_filters( 'activitypub_attachment_ids', $media, $this->wp_object );
224
225 $attchments = \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $media ) );
226
227 /**
228 * Filter the attachments for a post.
229 *
230 * @param array $attchments The attachments.
231 * @param WP_Post $this->wp_object The post object.
232 *
233 * @return array The filtered attachments.
234 */
235 return \apply_filters( 'activitypub_attachments', $attchments, $this->wp_object );
236 }
237
238 /**
239 * Get enclosures for a post.
240 *
241 * @param array $media The media array grouped by type.
242 *
243 * @return array The media array extended with enclosures.
244 */
245 public function get_enclosures( $media ) {
246 $enclosures = get_enclosures( $this->wp_object->ID );
247
248 if ( ! $enclosures ) {
249 return $media;
250 }
251
252 foreach ( $enclosures as $enclosure ) {
253 // check if URL is an attachment
254 $attachment_id = \attachment_url_to_postid( $enclosure['url'] );
255 if ( $attachment_id ) {
256 $enclosure['id'] = $attachment_id;
257 $enclosure['url'] = \wp_get_attachment_url( $attachment_id );
258 $enclosure['mediaType'] = \get_post_mime_type( $attachment_id );
259 }
260
261 $mime_type = $enclosure['mediaType'];
262 $mime_type_parts = \explode( '/', $mime_type );
263
264 switch ( $mime_type_parts[0] ) {
265 case 'image':
266 $media['image'][] = $enclosure;
267 break;
268 case 'audio':
269 $media['audio'][] = $enclosure;
270 break;
271 case 'video':
272 $media['video'][] = $enclosure;
273 break;
274 }
275 }
276
277 return $media;
278 }
279
280 /**
281 * Get media attachments from blocks. They will be formatted as ActivityPub attachments, not as WP attachments.
282 *
283 * @param array $media The media array grouped by type.
284 * @param int $max_media The maximum number of attachments to return.
285 *
286 * @return array The attachments.
287 */
288 protected function get_block_attachments( $media, $max_media ) {
289 // max media can't be negative or zero
290 if ( $max_media <= 0 ) {
291 return array();
292 }
293
294 $blocks = \parse_blocks( $this->wp_object->post_content );
295 $media = self::get_media_from_blocks( $blocks, $media );
296
297 return $media;
298 }
299
300 /**
301 * Recursively get media IDs from blocks.
302 * @param array $blocks The blocks to search for media IDs
303 * @param array $media The media IDs to append new IDs to
304 * @param int $max_media The maximum number of media to return.
305 *
306 * @return array The image IDs.
307 */
308 protected static function get_media_from_blocks( $blocks, $media ) {
309 foreach ( $blocks as $block ) {
310 // recurse into inner blocks
311 if ( ! empty( $block['innerBlocks'] ) ) {
312 $media = self::get_media_from_blocks( $block['innerBlocks'], $media );
313 }
314
315 switch ( $block['blockName'] ) {
316 case 'core/image':
317 case 'core/cover':
318 if ( ! empty( $block['attrs']['id'] ) ) {
319 $alt = '';
320 $check = preg_match( '/<img.*?alt\s*=\s*([\"\'])(.*?)\1.*>/i', $block['innerHTML'], $match );
321
322 if ( $check ) {
323 $alt = $match[2];
324 }
325
326 $media['image'][] = array(
327 'id' => $block['attrs']['id'],
328 'alt' => $alt,
329 );
330 }
331 break;
332 case 'core/audio':
333 if ( ! empty( $block['attrs']['id'] ) ) {
334 $media['audio'][] = array( 'id' => $block['attrs']['id'] );
335 }
336 break;
337 case 'core/video':
338 case 'videopress/video':
339 if ( ! empty( $block['attrs']['id'] ) ) {
340 $media['video'][] = array( 'id' => $block['attrs']['id'] );
341 }
342 break;
343 case 'jetpack/slideshow':
344 case 'jetpack/tiled-gallery':
345 if ( ! empty( $block['attrs']['ids'] ) ) {
346 $media['image'] = array_merge(
347 $media['image'],
348 array_map(
349 function ( $id ) {
350 return array( 'id' => $id );
351 },
352 $block['attrs']['ids']
353 )
354 );
355 }
356 break;
357 case 'jetpack/image-compare':
358 if ( ! empty( $block['attrs']['beforeImageId'] ) ) {
359 $media['image'][] = array( 'id' => $block['attrs']['beforeImageId'] );
360 }
361 if ( ! empty( $block['attrs']['afterImageId'] ) ) {
362 $media['image'][] = array( 'id' => $block['attrs']['afterImageId'] );
363 }
364 break;
365 }
366 }
367
368 return $media;
369 }
370
371 /**
372 * Get post images from the classic editor.
373 * Note that audio/video attachments are only supported in the block editor.
374 *
375 * @param array $media The media array grouped by type.
376 * @param int $max_images The maximum number of images to return.
377 *
378 * @return array The attachments.
379 */
380 protected function get_classic_editor_images( $media, $max_images ) {
381 // max images can't be negative or zero
382 if ( $max_images <= 0 ) {
383 return array();
384 }
385
386 if ( \count( $media['image'] ) <= $max_images ) {
387 if ( \class_exists( '\WP_HTML_Tag_Processor' ) ) {
388 $media['image'] = \array_merge( $media['image'], $this->get_classic_editor_image_embeds( $max_images ) );
389 } else {
390 $media['image'] = \array_merge( $media['image'], $this->get_classic_editor_image_attachments( $max_images ) );
391 }
392 }
393
394 return $media;
395 }
396
397 /**
398 * Get image embeds from the classic editor by parsing HTML.
399 *
400 * @param int $max_images The maximum number of images to return.
401 *
402 * @return array The attachments.
403 */
404 protected function get_classic_editor_image_embeds( $max_images ) {
405 // if someone calls that function directly, bail
406 if ( ! \class_exists( '\WP_HTML_Tag_Processor' ) ) {
407 return array();
408 }
409
410 // max images can't be negative or zero
411 if ( $max_images <= 0 ) {
412 return array();
413 }
414
415 $images = array();
416 $base = \wp_get_upload_dir()['baseurl'];
417 $content = \get_post_field( 'post_content', $this->wp_object );
418 $tags = new \WP_HTML_Tag_Processor( $content );
419
420 // This linter warning is a false positive - we have to
421 // re-count each time here as we modify $images.
422 // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
423 while ( $tags->next_tag( 'img' ) && ( \count( $images ) <= $max_images ) ) {
424 $src = $tags->get_attribute( 'src' );
425
426 // If the img source is in our uploads dir, get the
427 // associated ID. Note: if there's a -500x500
428 // type suffix, we remove it, but we try the original
429 // first in case the original image is actually called
430 // that. Likewise, we try adding the -scaled suffix for
431 // the case that this is a small version of an image
432 // that was big enough to get scaled down on upload:
433 // https://make.wordpress.org/core/2019/10/09/introducing-handling-of-big-images-in-wordpress-5-3/
434 if ( null !== $src && \str_starts_with( $src, $base ) ) {
435 $img_id = \attachment_url_to_postid( $src );
436
437 if ( 0 === $img_id ) {
438 $count = 0;
439 $src = preg_replace( '/-(?:\d+x\d+)(\.[a-zA-Z]+)$/', '$1', $src, 1, $count );
440 if ( $count > 0 ) {
441 $img_id = \attachment_url_to_postid( $src );
442 }
443 }
444
445 if ( 0 === $img_id ) {
446 $src = preg_replace( '/(\.[a-zA-Z]+)$/', '-scaled$1', $src );
447 $img_id = \attachment_url_to_postid( $src );
448 }
449
450 if ( 0 !== $img_id ) {
451 $images[] = array(
452 'id' => $img_id,
453 'alt' => $tags->get_attribute( 'alt' ),
454 );
455 }
456 }
457 }
458
459 return $images;
460 }
461
462 /**
463 * Get image attachments from the classic editor.
464 * This is imperfect as the contained images aren't necessarily the
465 * same as the attachments.
466 *
467 * @param int $max_images The maximum number of images to return.
468 *
469 * @return array The attachment IDs.
470 */
471 protected function get_classic_editor_image_attachments( $max_images ) {
472 // max images can't be negative or zero
473 if ( $max_images <= 0 ) {
474 return array();
475 }
476
477 $images = array();
478 $query = new \WP_Query(
479 array(
480 'post_parent' => $this->wp_object->ID,
481 'post_status' => 'inherit',
482 'post_type' => 'attachment',
483 'post_mime_type' => 'image',
484 'order' => 'ASC',
485 'orderby' => 'menu_order ID',
486 'posts_per_page' => $max_images,
487 )
488 );
489
490 foreach ( $query->get_posts() as $attachment ) {
491 if ( ! \in_array( $attachment->ID, $images, true ) ) {
492 $images[] = array( 'id' => $attachment->ID );
493 }
494 }
495
496 return $images;
497 }
498
499 /**
500 * Filter media IDs by object type.
501 *
502 * @param array $media The media array grouped by type.
503 * @param string $type The object type.
504 *
505 * @return array The filtered media IDs.
506 */
507 protected static function filter_media_by_object_type( $media, $type, $wp_object ) {
508 $type = \apply_filters( 'filter_media_by_object_type', \strtolower( $type ), $wp_object );
509
510 if ( ! empty( $media[ $type ] ) ) {
511 return $media[ $type ];
512 }
513
514 return array_filter( array_merge( ...array_values( $media ) ) );
515 }
516
517 /**
518 * Converts a WordPress Attachment to an ActivityPub Attachment.
519 *
520 * @param array $media The Attachment array.
521 *
522 * @return array The ActivityPub Attachment.
523 */
524 public static function wp_attachment_to_activity_attachment( $media ) {
525 if ( ! isset( $media['id'] ) ) {
526 return $media;
527 }
528
529 $id = $media['id'];
530 $attachment = array();
531 $mime_type = \get_post_mime_type( $id );
532 $mime_type_parts = \explode( '/', $mime_type );
533 // switching on image/audio/video
534 switch ( $mime_type_parts[0] ) {
535 case 'image':
536 $image_size = 'large';
537
538 /**
539 * Filter the image URL returned for each post.
540 *
541 * @param array|false $thumbnail The image URL, or false if no image is available.
542 * @param int $id The attachment ID.
543 * @param string $image_size The image size to retrieve. Set to 'large' by default.
544 */
545 $thumbnail = apply_filters(
546 'activitypub_get_image',
547 self::get_wordpress_attachment( $id, $image_size ),
548 $id,
549 $image_size
550 );
551
552 if ( $thumbnail ) {
553 $image = array(
554 'type' => 'Image',
555 'url' => \esc_url( $thumbnail[0] ),
556 'mediaType' => \esc_attr( $mime_type ),
557 );
558
559 if ( ! empty( $media['alt'] ) ) {
560 $image['name'] = \wp_strip_all_tags( \html_entity_decode( $media['alt'] ) );
561 } else {
562 $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
563 if ( $alt ) {
564 $image['name'] = \wp_strip_all_tags( \html_entity_decode( $alt ) );
565 }
566 }
567
568 $attachment = $image;
569 }
570 break;
571
572 case 'audio':
573 case 'video':
574 $attachment = array(
575 'type' => 'Document',
576 'mediaType' => \esc_attr( $mime_type ),
577 'url' => \esc_url( \wp_get_attachment_url( $id ) ),
578 'name' => \esc_attr( \get_the_title( $id ) ),
579 );
580 $meta = wp_get_attachment_metadata( $id );
581 // height and width for videos
582 if ( isset( $meta['width'] ) && isset( $meta['height'] ) ) {
583 $attachment['width'] = \esc_attr( $meta['width'] );
584 $attachment['height'] = \esc_attr( $meta['height'] );
585 }
586 // @todo: add `icon` support for audio/video attachments. Maybe use post thumbnail?
587 break;
588 }
589
590 return \apply_filters( 'activitypub_attachment', $attachment, $id );
591 }
592
593 /**
594 * Return details about an image attachment.
595 *
596 * @param int $id The attachment ID.
597 * @param string $image_size The image size to retrieve. Set to 'large' by default.
598 *
599 * @return array|false Array of image data, or boolean false if no image is available.
600 */
601 protected static function get_wordpress_attachment( $id, $image_size = 'large' ) {
602 /**
603 * Hook into the image retrieval process. Before image retrieval.
604 *
605 * @param int $id The attachment ID.
606 * @param string $image_size The image size to retrieve. Set to 'large' by default.
607 */
608 do_action( 'activitypub_get_image_pre', $id, $image_size );
609
610 $image = \wp_get_attachment_image_src( $id, $image_size );
611
612 /**
613 * Hook into the image retrieval process. After image retrieval.
614 *
615 * @param int $id The attachment ID.
616 * @param string $image_size The image size to retrieve. Set to 'large' by default.
617 */
618 do_action( 'activitypub_get_image_post', $id, $image_size );
619
620 return $image;
621 }
622
623 /**
624 * Returns the ActivityStreams 2.0 Object-Type for a Post based on the
625 * settings and the Post-Type.
626 *
627 * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
628 *
629 * @return string The Object-Type.
630 */
631 protected function get_type() {
632 $post_format_setting = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
633
634 if ( 'wordpress-post-format' !== $post_format_setting ) {
635 return \ucfirst( $post_format_setting );
636 }
637
638 $has_title = post_type_supports( $this->wp_object->post_type, 'title' );
639
640 if ( ! $has_title ) {
641 return 'Note';
642 }
643
644 // Default to Article.
645 $object_type = 'Article';
646 $post_format = 'standard';
647
648 if ( \get_theme_support( 'post-formats' ) ) {
649 $post_format = \get_post_format( $this->wp_object );
650 }
651
652 $post_type = \get_post_type( $this->wp_object );
653 switch ( $post_type ) {
654 case 'post':
655 switch ( $post_format ) {
656 case 'standard':
657 case '':
658 $object_type = 'Article';
659 break;
660 default:
661 $object_type = 'Note';
662 break;
663 }
664 break;
665 case 'page':
666 $object_type = 'Page';
667 break;
668 default:
669 $object_type = 'Article';
670 break;
671 }
672
673 return $object_type;
674 }
675
676 /**
677 * Returns a list of Mentions, used in the Post.
678 *
679 * @see https://docs.joinmastodon.org/spec/activitypub/#Mention
680 *
681 * @return array The list of Mentions.
682 */
683 protected function get_cc() {
684 $cc = array();
685
686 $mentions = $this->get_mentions();
687 if ( $mentions ) {
688 foreach ( $mentions as $url ) {
689 $cc[] = $url;
690 }
691 }
692
693 return $cc;
694 }
695
696
697 public function get_audience() {
698 if ( is_single_user() ) {
699 return null;
700 } else {
701 $blog = new Blog();
702 return $blog->get_id();
703 }
704 }
705
706 /**
707 * Returns a list of Tags, used in the Post.
708 *
709 * This includes Hash-Tags and Mentions.
710 *
711 * @return array The list of Tags.
712 */
713 protected function get_tag() {
714 $tags = array();
715
716 $post_tags = \get_the_tags( $this->wp_object->ID );
717 if ( $post_tags ) {
718 foreach ( $post_tags as $post_tag ) {
719 $tag = array(
720 'type' => 'Hashtag',
721 'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ),
722 'name' => esc_hashtag( $post_tag->name ),
723 );
724 $tags[] = $tag;
725 }
726 }
727
728 $mentions = $this->get_mentions();
729 if ( $mentions ) {
730 foreach ( $mentions as $mention => $url ) {
731 $tag = array(
732 'type' => 'Mention',
733 'href' => \esc_url( $url ),
734 'name' => \esc_html( $mention ),
735 );
736 $tags[] = $tag;
737 }
738 }
739
740 return $tags;
741 }
742
743 /**
744 * Returns the summary for the ActivityPub Item.
745 *
746 * The summary will be generated based on the user settings and only if the
747 * object type is not set to `note`.
748 *
749 * @return string|null The summary or null if the object type is `note`.
750 */
751 protected function get_summary() {
752 if ( 'Note' === $this->get_type() ) {
753 return null;
754 }
755
756 // Remove Teaser from drafts.
757 if ( 'draft' === \get_post_status( $this->wp_object ) ) {
758 return \__( '(This post is being modified)', 'activitypub' );
759 }
760
761 return generate_post_summary( $this->wp_object );
762 }
763
764 /**
765 * Returns the title for the ActivityPub Item.
766 *
767 * The title will be generated based on the user settings and only if the
768 * object type is not set to `note`.
769 *
770 * @return string|null The title or null if the object type is `note`.
771 */
772 protected function get_name() {
773 if ( 'Note' === $this->get_type() ) {
774 return null;
775 }
776
777 $title = \get_the_title( $this->wp_object->ID );
778
779 if ( $title ) {
780 return \wp_strip_all_tags(
781 \html_entity_decode(
782 $title
783 )
784 );
785 }
786
787 return null;
788 }
789
790 /**
791 * Returns the content for the ActivityPub Item.
792 *
793 * The content will be generated based on the user settings.
794 *
795 * @return string The content.
796 */
797 protected function get_content() {
798 add_filter( 'activitypub_reply_block', '__return_empty_string' );
799
800 // Remove Content from drafts.
801 if ( 'draft' === \get_post_status( $this->wp_object ) ) {
802 return \__( '(This post is being modified)', 'activitypub' );
803 }
804
805 global $post;
806
807 /**
808 * Provides an action hook so plugins can add their own hooks/filters before AP content is generated.
809 *
810 * 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.
811 *
812 * @param WP_Post $post The post object.
813 */
814 do_action( 'activitypub_before_get_content', $post );
815
816 add_filter( 'render_block_core/embed', array( self::class, 'revert_embed_links' ), 10, 2 );
817
818 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
819 $post = $this->wp_object;
820 $content = $this->get_post_content_template();
821
822 // Register our shortcodes just in time.
823 Shortcodes::register();
824 // Fill in the shortcodes.
825 setup_postdata( $post );
826 $content = do_shortcode( $content );
827 wp_reset_postdata();
828
829 $content = \wpautop( $content );
830 $content = \preg_replace( '/[\n\r\t]/', '', $content );
831 $content = \trim( $content );
832
833 $content = \apply_filters( 'activitypub_the_content', $content, $post );
834
835 // Don't need these any more, should never appear in a post.
836 Shortcodes::unregister();
837
838 return $content;
839 }
840
841 /**
842 * Gets the template to use to generate the content of the activitypub item.
843 *
844 * @return string The Template.
845 */
846 protected function get_post_content_template() {
847 $type = \get_option( 'activitypub_post_content_type', 'content' );
848
849 switch ( $type ) {
850 case 'excerpt':
851 $template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
852 break;
853 case 'title':
854 $template = "<h2>[ap_title]</h2>\n\n[ap_permalink type=\"html\"]";
855 break;
856 case 'content':
857 $template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
858 break;
859 default:
860 // phpcs:ignore Universal.Operators.DisallowShortTernary.Found
861 $template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ) ?: ACTIVITYPUB_CUSTOM_POST_CONTENT;
862 break;
863 }
864
865 $post_format_setting = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
866
867 if ( 'wordpress-post-format' === $post_format_setting ) {
868 $template = '[ap_content]';
869 }
870
871 return apply_filters( 'activitypub_object_content_template', $template, $this->wp_object );
872 }
873
874 /**
875 * Helper function to get the @-Mentions from the post content.
876 *
877 * @return array The list of @-Mentions.
878 */
879 protected function get_mentions() {
880 return apply_filters(
881 'activitypub_extract_mentions',
882 array(),
883 $this->wp_object->post_content . ' ' . $this->wp_object->post_excerpt,
884 $this->wp_object
885 );
886 }
887
888 /**
889 * Returns the locale of the post.
890 *
891 * @return string The locale of the post.
892 */
893 public function get_locale() {
894 $post_id = $this->wp_object->ID;
895 $lang = \strtolower( \strtok( \get_locale(), '_-' ) );
896
897 /**
898 * Filter the locale of the post.
899 *
900 * @param string $lang The locale of the post.
901 * @param int $post_id The post ID.
902 * @param WP_Post $post The post object.
903 *
904 * @return string The filtered locale of the post.
905 */
906 return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->wp_object );
907 }
908
909 /**
910 * Returns the in-reply-to URL of the post.
911 *
912 * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-inreplyto
913 *
914 * @return string|null The in-reply-to URL of the post.
915 */
916 public function get_in_reply_to() {
917 $blocks = \parse_blocks( $this->wp_object->post_content );
918
919 foreach ( $blocks as $block ) {
920 if ( 'activitypub/reply' === $block['blockName'] ) {
921 // We only support one reply block per post for now.
922 return $block['attrs']['url'];
923 }
924 }
925
926 return null;
927 }
928
929 /**
930 * Transform Embed blocks to block level link.
931 *
932 * Remote servers will simply drop iframe elements, rendering incomplete content.
933 *
934 * @see https://www.w3.org/TR/activitypub/#security-sanitizing-content
935 * @see https://www.w3.org/wiki/ActivityPub/Primer/HTML
936 *
937 * @param string $block_content The block content (html)
938 * @param object $block The block object
939 *
940 * @return string A block level link
941 */
942 public static function revert_embed_links( $block_content, $block ) {
943 return '<p><a href="' . esc_url( $block['attrs']['url'] ) . '">' . $block['attrs']['url'] . '</a></p>';
944 }
945 }
946