PluginProbe
ActivityPub / 4.1.1
ActivityPub v4.1.1
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / transformer / class-post.php

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

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