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