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