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 +534 -195 1.3.03.2.5 View file →
@@ -1,22 +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\Shortcodes;
8 +use Activitypub\Transformer\Base;
9 9
10 10 use function Activitypub\esc_hashtag;
11 11 use function Activitypub\is_single_user;
12 +use function Activitypub\get_enclosures;
13 +use function Activitypub\site_supports_blocks;
12 14 use function Activitypub\get_rest_url_by_path;
13 -use function Activitypub\site_supports_blocks;
15 +use function Activitypub\is_user_type_disabled;
16 +use function Activitypub\generate_post_summary;
14 17
15 18 /**
16 19 * WordPress Post Transformer
17 20 *
18 - * 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
19 22 * Object-Types.
20 23 *
21 24 * Currently supported are:
22 25 *
@@ -21,37 +24,34 @@
21 24 * Currently supported are:
22 25 *
23 26 * - Activitypub\Activity\Base_Object
24 27 */
25 -class Post {
26 -
28 +class Post extends Base {
27 29 /**
28 - * The WP_Post object.
30 + * The User as Actor Object.
29 31 *
30 - * @var WP_Post
32 + * @var Activitypub\Activity\Actor
31 33 */
32 - protected $wp_post;
34 + private $actor_object = null;
33 35
34 36 /**
35 - * Static function to Transform a WP_Post Object.
37 + * Returns the ID of the WordPress Post.
36 38 *
37 - * This helps to chain the output of the Transformer.
38 - *
39 - * @param WP_Post $wp_post The WP_Post object
40 - *
41 - * @return void
39 + * @return int The ID of the WordPress Post
42 40 */
43 - public static function transform( WP_Post $wp_post ) {
44 - return new static( $wp_post );
41 + public function get_wp_user_id() {
42 + return $this->wp_object->post_author;
45 43 }
46 44
47 45 /**
46 + * Change the User-ID of the WordPress Post.
48 47 *
49 - *
50 - * @param WP_Post $wp_post
48 + * @return int The User-ID of the WordPress Post
51 49 */
52 - public function __construct( WP_Post $wp_post ) {
53 - $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;
54 54 }
55 55
56 56 /**
57 57 * Transforms the WP_Post object to an ActivityPub Object
@@ -60,48 +60,67 @@
60 60 *
61 61 * @return \Activitypub\Activity\Base_Object The ActivityPub Object
62 62 */
63 63 public function to_object() {
64 - $wp_post = $this->wp_post;
65 - $object = new Base_Object();
64 + $post = $this->wp_object;
65 + $object = parent::to_object();
66 66
67 - $object->set_id( $this->get_id() );
68 - $object->set_url( $this->get_url() );
69 - $object->set_type( $this->get_object_type() );
67 + $published = \strtotime( $post->post_date_gmt );
70 68
71 - $published = \strtotime( $wp_post->post_date_gmt );
72 -
73 69 $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) );
74 70
75 - $updated = \strtotime( $wp_post->post_modified_gmt );
71 + $updated = \strtotime( $post->post_modified_gmt );
76 72
77 73 if ( $updated > $published ) {
78 74 $object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) );
79 75 }
80 76
81 - $object->set_attributed_to( $this->get_attributed_to() );
82 - $object->set_content( $this->get_content() );
83 77 $object->set_content_map(
84 78 array(
85 79 $this->get_locale() => $this->get_content(),
86 80 )
87 81 );
88 - $path = sprintf( 'users/%d/followers', intval( $wp_post->post_author ) );
89 82
90 83 $object->set_to(
91 84 array(
92 85 'https://www.w3.org/ns/activitystreams#Public',
93 - get_rest_url_by_path( $path ),
86 + $this->get_actor_object()->get_followers(),
94 87 )
95 88 );
96 - $object->set_cc( $this->get_cc() );
97 - $object->set_attachment( $this->get_attachments() );
98 - $object->set_tag( $this->get_tags() );
99 89
100 90 return $object;
101 91 }
102 92
103 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 + /**
104 123 * Returns the ID of the Post.
105 124 *
106 125 * @return string The Posts ID.
107 126 */
@@ -114,14 +133,25 @@
114 133 *
115 134 * @return string The Posts URL.
116 135 */
117 136 public function get_url() {
118 - $post = $this->wp_post;
137 + $post = $this->wp_object;
119 138
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 );
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;
124 154 }
125 155
126 156 return \esc_url( $permalink );
127 157 }
@@ -133,14 +163,9 @@
133 163 *
134 164 * @return string The User-URL.
135 165 */
136 166 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();
167 + return $this->get_actor_object()->get_url();
143 168 }
144 169
145 170 /**
146 171 * Generates all Media Attachments for a Post.
@@ -146,193 +171,401 @@
146 171 * Generates all Media Attachments for a Post.
147 172 *
148 173 * @return array The Attachments.
149 174 */
150 - protected function get_attachments() {
175 + protected function get_attachment() {
176 + // Remove attachments from drafts.
177 + if ( 'draft' === \get_post_status( $this->wp_object ) ) {
178 + return array();
179 + }
180 +
151 181 // Once upon a time we only supported images, but we now support audio/video as well.
152 182 // 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 ) ) );
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 + );
154 189
155 - if ( site_supports_blocks() && \has_blocks( $this->wp_post->post_content ) ) {
156 - return $this->get_block_attachments( $max_media );
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 ) );
157 200 }
158 201
159 - return $this->get_classic_editor_images( $max_media );
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 );
160 236 }
161 237
162 238 /**
163 - * Get media attachments from blocks. They will be formatted as ActivityPub attachments, not as WP attachments.
239 + * Get enclosures for a post.
164 240 *
165 - * @param int $max_media The maximum number of attachments to return.
241 + * @param array $media The media array grouped by type.
166 242 *
167 - * @return array The attachments.
243 + * @return array The media array extended with enclosures.
168 244 */
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();
245 + public function get_enclosures( $media ) {
246 + $enclosures = get_enclosures( $this->wp_object->ID );
247 +
248 + if ( ! $enclosures ) {
249 + return $media;
173 250 }
174 251
175 - $id = $this->wp_post->ID;
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 + }
176 260
177 - $media_ids = array();
261 + $mime_type = $enclosure['mediaType'];
262 + $mime_type_parts = \explode( '/', $mime_type );
178 263
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 );
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 + }
182 275 }
183 276
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 ) );
277 + return $media;
190 278 }
191 279
192 280 /**
193 - * Get image attachments from the classic editor.
194 - * Note that audio/video attachments are only supported in the block editor.
281 + * Get media attachments from blocks. They will be formatted as ActivityPub attachments, not as WP attachments.
195 282 *
196 - * @param int $max_images The maximum number of images to return.
283 + * @param array $media The media array grouped by type.
284 + * @param int $max_media The maximum number of attachments to return.
197 285 *
198 286 * @return array The attachments.
199 287 */
200 - protected function get_classic_editor_images( $max_images ) {
201 - // max images can't be negative or zero
202 - if ( $max_images <= 0 ) {
288 + protected function get_block_attachments( $media, $max_media ) {
289 + // max media can't be negative or zero
290 + if ( $max_media <= 0 ) {
203 291 return array();
204 292 }
205 293
206 - $id = $this->wp_post->ID;
294 + $blocks = \parse_blocks( $this->wp_object->post_content );
295 + $media = self::get_media_from_blocks( $blocks, $media );
207 296
208 - $image_ids = array();
209 -
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;
214 - }
215 -
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 - }
233 - }
234 - $image_ids = \array_unique( $image_ids );
235 -
236 - return \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $image_ids ) );
297 + return $media;
237 298 }
238 299
239 300 /**
240 301 * Recursively get media IDs from blocks.
241 302 * @param array $blocks The blocks to search for media IDs
242 - * @param array $media_ids The media IDs to append new IDs to
303 + * @param array $media The media IDs to append new IDs to
243 304 * @param int $max_media The maximum number of media to return.
244 305 *
245 306 * @return array The image IDs.
246 307 */
247 - protected static function get_media_ids_from_blocks( $blocks, $media_ids, $max_media ) {
248 -
308 + protected static function get_media_from_blocks( $blocks, $media ) {
249 309 foreach ( $blocks as $block ) {
250 310 // recurse into inner blocks
251 311 if ( ! empty( $block['innerBlocks'] ) ) {
252 - $media_ids = self::get_media_ids_from_blocks( $block['innerBlocks'], $media_ids, $max_media );
312 + $media = self::get_media_from_blocks( $block['innerBlocks'], $media );
253 313 }
254 314
255 315 switch ( $block['blockName'] ) {
256 316 case 'core/image':
257 317 case 'core/cover':
318 + if ( ! empty( $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 + );
330 + }
331 + break;
258 332 case 'core/audio':
333 + if ( ! empty( $block['attrs']['id'] ) ) {
334 + $media['audio'][] = array( 'id' => $block['attrs']['id'] );
335 + }
336 + break;
259 337 case 'core/video':
260 338 case 'videopress/video':
261 339 if ( ! empty( $block['attrs']['id'] ) ) {
262 - $media_ids[] = $block['attrs']['id'];
340 + $media['video'][] = array( 'id' => $block['attrs']['id'] );
263 341 }
264 342 break;
265 343 case 'jetpack/slideshow':
266 344 case 'jetpack/tiled-gallery':
267 345 if ( ! empty( $block['attrs']['ids'] ) ) {
268 - $media_ids = array_merge( $media_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 + );
269 355 }
270 356 break;
271 357 case 'jetpack/image-compare':
272 358 if ( ! empty( $block['attrs']['beforeImageId'] ) ) {
273 - $media_ids[] = $block['attrs']['beforeImageId'];
359 + $media['image'][] = array( 'id' => $block['attrs']['beforeImageId'] );
274 360 }
275 361 if ( ! empty( $block['attrs']['afterImageId'] ) ) {
276 - $media_ids[] = $block['attrs']['afterImageId'];
362 + $media['image'][] = array( 'id' => $block['attrs']['afterImageId'] );
277 363 }
278 364 break;
279 365 }
366 + }
280 367
281 - // depupe
282 - $media_ids = \array_unique( $media_ids );
368 + return $media;
369 + }
283 370
284 - // stop doing unneeded work
285 - if ( count( $media_ids ) >= $max_media ) {
286 - break;
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 ) );
287 391 }
288 392 }
289 393
290 - // still need to slice it because one gallery could knock us over the limit
291 - return array_slice( $media_ids, 0, $max_media );
394 + return $media;
292 395 }
293 396
294 397 /**
398 + * Get image embeds from the classic editor by parsing HTML.
399 + *
400 + * @param int $max_images The maximum number of images to return.
401 + *
402 + * @return array The attachments.
403 + */
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 + }
409 +
410 + // max images can't be negative or zero
411 + if ( $max_images <= 0 ) {
412 + return array();
413 + }
414 +
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 );
419 +
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' );
425 +
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 );
436 +
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 );
442 + }
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 + }
456 + }
457 + }
458 +
459 + return $images;
460 + }
461 +
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 + }
476 +
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 + );
489 +
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 + /**
295 518 * Converts a WordPress Attachment to an ActivityPub Attachment.
296 519 *
297 - * @param int $id The Attachment ID.
520 + * @param array $media The Attachment array.
298 521 *
299 522 * @return array The ActivityPub Attachment.
300 523 */
301 - public static function wp_attachment_to_activity_attachment( $id ) {
302 - $attachment = array();
303 - $mime_type = \get_post_mime_type( $id );
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 );
304 532 $mime_type_parts = \explode( '/', $mime_type );
305 533 // switching on image/audio/video
306 534 switch ( $mime_type_parts[0] ) {
307 535 case 'image':
308 - $image_size = 'full';
536 + $image_size = 'large';
309 537
310 538 /**
311 539 * Filter the image URL returned for each post.
312 540 *
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.
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.
316 544 */
317 545 $thumbnail = apply_filters(
318 546 'activitypub_get_image',
319 - self::get_image( $id, $image_size ),
547 + self::get_wordpress_attachment( $id, $image_size ),
320 548 $id,
321 549 $image_size
322 550 );
323 551
324 552 if ( $thumbnail ) {
325 - $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
326 553 $image = array(
327 554 'type' => 'Image',
328 - 'url' => $thumbnail[0],
329 - 'mediaType' => $mime_type,
555 + 'url' => \esc_url( $thumbnail[0] ),
556 + 'mediaType' => \esc_attr( $mime_type ),
330 557 );
331 558
332 - if ( $alt ) {
333 - $image['name'] = $alt;
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 + }
334 566 }
567 +
335 568 $attachment = $image;
336 569 }
337 570 break;
338 571
@@ -339,17 +572,17 @@
339 572 case 'audio':
340 573 case 'video':
341 574 $attachment = array(
342 575 'type' => 'Document',
343 - 'mediaType' => $mime_type,
344 - 'url' => \wp_get_attachment_url( $id ),
345 - 'name' => \get_the_title( $id ),
576 + 'mediaType' => \esc_attr( $mime_type ),
577 + 'url' => \esc_url( \wp_get_attachment_url( $id ) ),
578 + 'name' => \esc_attr( \get_the_title( $id ) ),
346 579 );
347 580 $meta = wp_get_attachment_metadata( $id );
348 581 // height and width for videos
349 582 if ( isset( $meta['width'] ) && isset( $meta['height'] ) ) {
350 - $attachment['width'] = $meta['width'];
351 - $attachment['height'] = $meta['height'];
583 + $attachment['width'] = \esc_attr( $meta['width'] );
584 + $attachment['height'] = \esc_attr( $meta['height'] );
352 585 }
353 586 // @todo: add `icon` support for audio/video attachments. Maybe use post thumbnail?
354 587 break;
355 588 }
@@ -360,18 +593,18 @@
360 593 /**
361 594 * Return details about an image attachment.
362 595 *
363 596 * @param int $id The attachment ID.
364 - * @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.
365 598 *
366 599 * @return array|false Array of image data, or boolean false if no image is available.
367 600 */
368 - protected static function get_image( $id, $image_size = 'full' ) {
601 + protected static function get_wordpress_attachment( $id, $image_size = 'large' ) {
369 602 /**
370 603 * Hook into the image retrieval process. Before image retrieval.
371 604 *
372 605 * @param int $id The attachment ID.
373 - * @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.
374 607 */
375 608 do_action( 'activitypub_get_image_pre', $id, $image_size );
376 609
377 610 $image = \wp_get_attachment_image_src( $id, $image_size );
@@ -379,9 +612,9 @@
379 612 /**
380 613 * Hook into the image retrieval process. After image retrieval.
381 614 *
382 615 * @param int $id The attachment ID.
383 - * @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.
384 617 */
385 618 do_action( 'activitypub_get_image_post', $id, $image_size );
386 619
387 620 return $image;
@@ -394,38 +627,39 @@
394 627 * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
395 628 *
396 629 * @return string The Object-Type.
397 630 */
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' ) );
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 );
401 636 }
402 637
638 + $has_title = post_type_supports( $this->wp_object->post_type, 'title' );
639 +
640 + if ( ! $has_title ) {
641 + return 'Note';
642 + }
643 +
403 644 // Default to Article.
404 645 $object_type = 'Article';
405 - $post_type = \get_post_type( $this->wp_post );
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 );
406 653 switch ( $post_type ) {
407 654 case 'post':
408 - $post_format = \get_post_format( $this->wp_post );
409 655 switch ( $post_format ) {
410 - case 'aside':
411 - case 'status':
412 - case 'quote':
413 - case 'note':
414 - $object_type = 'Note';
656 + case 'standard':
657 + case '':
658 + $object_type = 'Article';
415 659 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 660 default:
427 - $object_type = 'Article';
661 + $object_type = 'Note';
428 662 break;
429 663 }
430 664 break;
431 665 case 'page':
@@ -430,23 +664,8 @@
430 664 break;
431 665 case 'page':
432 666 $object_type = 'Page';
433 667 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 668 default:
450 669 $object_type = 'Article';
451 670 break;
452 671 }
@@ -473,8 +692,18 @@
473 692
474 693 return $cc;
475 694 }
476 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 +
477 706 /**
478 707 * Returns a list of Tags, used in the Post.
479 708 *
480 709 * This includes Hash-Tags and Mentions.
@@ -480,12 +709,12 @@
480 709 * This includes Hash-Tags and Mentions.
481 710 *
482 711 * @return array The list of Tags.
483 712 */
484 - protected function get_tags() {
713 + protected function get_tag() {
485 714 $tags = array();
486 715
487 - $post_tags = \get_the_tags( $this->wp_post->ID );
716 + $post_tags = \get_the_tags( $this->wp_object->ID );
488 717 if ( $post_tags ) {
489 718 foreach ( $post_tags as $post_tag ) {
490 719 $tag = array(
491 720 'type' => 'Hashtag',
@@ -511,8 +740,55 @@
511 740 return $tags;
512 741 }
513 742
514 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 + /**
515 791 * Returns the content for the ActivityPub Item.
516 792 *
517 793 * The content will be generated based on the user settings.
518 794 *
@@ -518,8 +794,15 @@
518 794 *
519 795 * @return string The content.
520 796 */
521 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 +
522 805 global $post;
523 806
524 807 /**
525 808 * Provides an action hook so plugins can add their own hooks/filters before AP content is generated.
@@ -529,10 +812,12 @@
529 812 * @param WP_Post $post The post object.
530 813 */
531 814 do_action( 'activitypub_before_get_content', $post );
532 815
816 + add_filter( 'render_block_core/embed', array( self::class, 'revert_embed_links' ), 10, 2 );
817 +
533 818 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
534 - $post = $this->wp_post;
819 + $post = $this->wp_object;
535 820 $content = $this->get_post_content_template();
536 821
537 822 // Register our shortcodes just in time.
538 823 Shortcodes::register();
@@ -558,21 +843,33 @@
558 843 *
559 844 * @return string The Template.
560 845 */
561 846 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\"]";
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;
564 863 }
565 864
566 - if ( 'title' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
567 - return "[ap_title]\n\n[ap_permalink type=\"html\"]";
568 - }
865 + $post_format_setting = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
569 866
570 - if ( 'content' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
571 - 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]';
572 869 }
573 870
574 - return \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
871 + return apply_filters( 'activitypub_object_content_template', $template, $this->wp_object );
575 872 }
576 873
577 874 /**
578 875 * Helper function to get the @-Mentions from the post content.
@@ -579,9 +876,14 @@
579 876 *
580 877 * @return array The list of @-Mentions.
581 878 */
582 879 protected function get_mentions() {
583 - 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 + );
584 886 }
585 887
586 888 /**
587 889 * Returns the locale of the post.
@@ -588,9 +890,9 @@
588 890 *
589 891 * @return string The locale of the post.
590 892 */
591 893 public function get_locale() {
592 - $post_id = $this->wp_post->ID;
894 + $post_id = $this->wp_object->ID;
593 895 $lang = \strtolower( \strtok( \get_locale(), '_-' ) );
594 896
595 897 /**
596 898 * Filter the locale of the post.
@@ -600,7 +902,44 @@
600 902 * @param WP_Post $post The post object.
601 903 *
602 904 * @return string The filtered locale of the post.
603 905 */
604 - return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->wp_post );
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>';
605 944 }
606 945 }