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