PluginProbe
ActivityPub / 5.1.0
ActivityPub v5.1.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 5.1.0, at includes/transformer/class-post.php

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