PluginProbe
ActivityPub / 1.2.0
ActivityPub v1.2.0
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / transformer / class-post.php

class-post.php in ActivityPub 1.2.0, at includes/transformer/class-post.php

607 lines 15.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Activitypub\Transformer;
3
4 use WP_Post;
5 use Activitypub\Collection\Users;
6 use Activitypub\Model\Blog_User;
7 use Activitypub\Activity\Base_Object;
8 use Activitypub\Shortcodes;
9
10 use function Activitypub\esc_hashtag;
11 use function Activitypub\is_single_user;
12 use function Activitypub\get_rest_url_by_path;
13 use function Activitypub\site_supports_blocks;
14
15 /**
16 * WordPress Post Transformer
17 *
18 * The Post Transformer is responsible for transforming a WP_Post object into different othe
19 * Object-Types.
20 *
21 * Currently supported are:
22 *
23 * - Activitypub\Activity\Base_Object
24 */
25 class Post {
26
27 /**
28 * The WP_Post object.
29 *
30 * @var WP_Post
31 */
32 protected $wp_post;
33
34 /**
35 * Static function to Transform a WP_Post Object.
36 *
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
42 */
43 public static function transform( WP_Post $wp_post ) {
44 return new static( $wp_post );
45 }
46
47 /**
48 *
49 *
50 * @param WP_Post $wp_post
51 */
52 public function __construct( WP_Post $wp_post ) {
53 $this->wp_post = $wp_post;
54 }
55
56 /**
57 * Transforms the WP_Post object to an ActivityPub Object
58 *
59 * @see \Activitypub\Activity\Base_Object
60 *
61 * @return \Activitypub\Activity\Base_Object The ActivityPub Object
62 */
63 public function to_object() {
64 $wp_post = $this->wp_post;
65 $object = new Base_Object();
66
67 $object->set_id( $this->get_id() );
68 $object->set_url( $this->get_url() );
69 $object->set_type( $this->get_object_type() );
70
71 $published = \strtotime( $wp_post->post_date_gmt );
72
73 $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) );
74
75 $updated = \strtotime( $wp_post->post_modified_gmt );
76
77 if ( $updated > $published ) {
78 $object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) );
79 }
80
81 $object->set_attributed_to( $this->get_attributed_to() );
82 $object->set_content( $this->get_content() );
83 $object->set_content_map(
84 array(
85 $this->get_locale() => $this->get_content(),
86 )
87 );
88 $path = sprintf( 'users/%d/followers', intval( $wp_post->post_author ) );
89
90 $object->set_to(
91 array(
92 'https://www.w3.org/ns/activitystreams#Public',
93 get_rest_url_by_path( $path ),
94 )
95 );
96 $object->set_cc( $this->get_cc() );
97 $object->set_attachment( $this->get_attachments() );
98 $object->set_tag( $this->get_tags() );
99
100 return $object;
101 }
102
103 /**
104 * Returns the ID of the Post.
105 *
106 * @return string The Posts ID.
107 */
108 public function get_id() {
109 return $this->get_url();
110 }
111
112 /**
113 * Returns the URL of the Post.
114 *
115 * @return string The Posts URL.
116 */
117 public function get_url() {
118 $post = $this->wp_post;
119
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 );
124 }
125
126 return \esc_url( $permalink );
127 }
128
129 /**
130 * Returns the User-URL of the Author of the Post.
131 *
132 * If `single_user` mode is enabled, the URL of the Blog-User is returned.
133 *
134 * @return string The User-URL.
135 */
136 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();
143 }
144
145 /**
146 * Generates all Media Attachments for a Post.
147 *
148 * @return array The Attachments.
149 */
150 protected function get_attachments() {
151 // Once upon a time we only supported images, but we now support audio/video as well.
152 // We maintain the image-centric naming for backwards compatibility.
153 $max_media = intval( \apply_filters( 'activitypub_max_image_attachments', \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ) );
154
155 if ( site_supports_blocks() && \has_blocks( $this->wp_post->post_content ) ) {
156 return $this->get_block_attachments( $max_media );
157 }
158
159 return $this->get_classic_editor_images( $max_media );
160 }
161
162 /**
163 * Get media attachments from blocks. They will be formatted as ActivityPub attachments, not as WP attachments.
164 *
165 * @param int $max_media The maximum number of attachments to return.
166 *
167 * @return array The attachments.
168 */
169 protected function get_block_attachments( $max_media ) {
170 // max media can't be negative or zero
171 if ( $max_media <= 0 ) {
172 return array();
173 }
174
175 $id = $this->wp_post->ID;
176
177 $media_ids = array();
178
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 );
182 }
183
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 ) );
190 }
191
192 /**
193 * Get image attachments from the classic editor.
194 * Note that audio/video attachments are only supported in the block editor.
195 *
196 * @param int $max_images The maximum number of images to return.
197 *
198 * @return array The attachments.
199 */
200 protected function get_classic_editor_images( $max_images ) {
201 // max images can't be negative or zero
202 if ( $max_images <= 0 ) {
203 return array();
204 }
205
206 $id = $this->wp_post->ID;
207
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 ) );
237 }
238
239 /**
240 * Recursively get media IDs from blocks.
241 * @param array $blocks The blocks to search for media IDs
242 * @param array $media_ids The media IDs to append new IDs to
243 * @param int $max_media The maximum number of media to return.
244 *
245 * @return array The image IDs.
246 */
247 protected static function get_media_ids_from_blocks( $blocks, $media_ids, $max_media ) {
248
249 foreach ( $blocks as $block ) {
250 // recurse into inner blocks
251 if ( ! empty( $block['innerBlocks'] ) ) {
252 $media_ids = self::get_media_ids_from_blocks( $block['innerBlocks'], $media_ids, $max_media );
253 }
254
255 switch ( $block['blockName'] ) {
256 case 'core/image':
257 case 'core/cover':
258 case 'core/audio':
259 case 'core/video':
260 case 'videopress/video':
261 if ( ! empty( $block['attrs']['id'] ) ) {
262 $media_ids[] = $block['attrs']['id'];
263 }
264 break;
265 case 'jetpack/slideshow':
266 case 'jetpack/tiled-gallery':
267 if ( ! empty( $block['attrs']['ids'] ) ) {
268 $media_ids = array_merge( $media_ids, $block['attrs']['ids'] );
269 }
270 break;
271 case 'jetpack/image-compare':
272 if ( ! empty( $block['attrs']['beforeImageId'] ) ) {
273 $media_ids[] = $block['attrs']['beforeImageId'];
274 }
275 if ( ! empty( $block['attrs']['afterImageId'] ) ) {
276 $media_ids[] = $block['attrs']['afterImageId'];
277 }
278 break;
279 }
280
281 // depupe
282 $media_ids = \array_unique( $media_ids );
283
284 // stop doing unneeded work
285 if ( count( $media_ids ) >= $max_media ) {
286 break;
287 }
288 }
289
290 // still need to slice it because one gallery could knock us over the limit
291 return array_slice( $media_ids, 0, $max_media );
292 }
293
294 /**
295 * Converts a WordPress Attachment to an ActivityPub Attachment.
296 *
297 * @param int $id The Attachment ID.
298 *
299 * @return array The ActivityPub Attachment.
300 */
301 public static function wp_attachment_to_activity_attachment( $id ) {
302 $attachment = array();
303 $mime_type = \get_post_mime_type( $id );
304 $mime_type_parts = \explode( '/', $mime_type );
305 // switching on image/audio/video
306 switch ( $mime_type_parts[0] ) {
307 case 'image':
308 $image_size = 'full';
309
310 /**
311 * Filter the image URL returned for each post.
312 *
313 * @param array|false $thumbnail The image URL, or false if no image is available.
314 * @param int $id The attachment ID.
315 * @param string $image_size The image size to retrieve. Set to 'full' by default.
316 */
317 $thumbnail = apply_filters(
318 'activitypub_get_image',
319 self::get_image( $id, $image_size ),
320 $id,
321 $image_size
322 );
323
324 if ( $thumbnail ) {
325 $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
326 $image = array(
327 'type' => 'Image',
328 'url' => $thumbnail[0],
329 'mediaType' => $mime_type,
330 );
331
332 if ( $alt ) {
333 $image['name'] = $alt;
334 }
335 $attachment = $image;
336 }
337 break;
338
339 case 'audio':
340 case 'video':
341 $attachment = array(
342 'type' => 'Document',
343 'mediaType' => $mime_type,
344 'url' => \wp_get_attachment_url( $id ),
345 'name' => \get_the_title( $id ),
346 );
347 $meta = wp_get_attachment_metadata( $id );
348 // height and width for videos
349 if ( isset( $meta['width'] ) && isset( $meta['height'] ) ) {
350 $attachment['width'] = $meta['width'];
351 $attachment['height'] = $meta['height'];
352 }
353 // @todo: add `icon` support for audio/video attachments. Maybe use post thumbnail?
354 break;
355 }
356
357 return \apply_filters( 'activitypub_attachment', $attachment, $id );
358 }
359
360 /**
361 * Return details about an image attachment.
362 *
363 * @param int $id The attachment ID.
364 * @param string $image_size The image size to retrieve. Set to 'full' by default.
365 *
366 * @return array|false Array of image data, or boolean false if no image is available.
367 */
368 protected static function get_image( $id, $image_size = 'full' ) {
369 /**
370 * Hook into the image retrieval process. Before image retrieval.
371 *
372 * @param int $id The attachment ID.
373 * @param string $image_size The image size to retrieve. Set to 'full' by default.
374 */
375 do_action( 'activitypub_get_image_pre', $id, $image_size );
376
377 $image = \wp_get_attachment_image_src( $id, $image_size );
378
379 /**
380 * Hook into the image retrieval process. After image retrieval.
381 *
382 * @param int $id The attachment ID.
383 * @param string $image_size The image size to retrieve. Set to 'full' by default.
384 */
385 do_action( 'activitypub_get_image_post', $id, $image_size );
386
387 return $image;
388 }
389
390 /**
391 * Returns the ActivityStreams 2.0 Object-Type for a Post based on the
392 * settings and the Post-Type.
393 *
394 * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
395 *
396 * @return string The Object-Type.
397 */
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' ) );
401 }
402
403 // Default to Article.
404 $object_type = 'Article';
405 $post_type = \get_post_type( $this->wp_post );
406 switch ( $post_type ) {
407 case 'post':
408 $post_format = \get_post_format( $this->wp_post );
409 switch ( $post_format ) {
410 case 'aside':
411 case 'status':
412 case 'quote':
413 case 'note':
414 $object_type = 'Note';
415 break;
416 case 'gallery':
417 case 'image':
418 $object_type = 'Image';
419 break;
420 case 'video':
421 $object_type = 'Video';
422 break;
423 case 'audio':
424 $object_type = 'Audio';
425 break;
426 default:
427 $object_type = 'Article';
428 break;
429 }
430 break;
431 case 'page':
432 $object_type = 'Page';
433 break;
434 case 'attachment':
435 $mime_type = \get_post_mime_type();
436 $media_type = \preg_replace( '/(\/[a-zA-Z]+)/i', '', $mime_type );
437 switch ( $media_type ) {
438 case 'audio':
439 $object_type = 'Audio';
440 break;
441 case 'video':
442 $object_type = 'Video';
443 break;
444 case 'image':
445 $object_type = 'Image';
446 break;
447 }
448 break;
449 default:
450 $object_type = 'Article';
451 break;
452 }
453
454 return $object_type;
455 }
456
457 /**
458 * Returns a list of Mentions, used in the Post.
459 *
460 * @see https://docs.joinmastodon.org/spec/activitypub/#Mention
461 *
462 * @return array The list of Mentions.
463 */
464 protected function get_cc() {
465 $cc = array();
466
467 $mentions = $this->get_mentions();
468 if ( $mentions ) {
469 foreach ( $mentions as $url ) {
470 $cc[] = $url;
471 }
472 }
473
474 return $cc;
475 }
476
477 /**
478 * Returns a list of Tags, used in the Post.
479 *
480 * This includes Hash-Tags and Mentions.
481 *
482 * @return array The list of Tags.
483 */
484 protected function get_tags() {
485 $tags = array();
486
487 $post_tags = \get_the_tags( $this->wp_post->ID );
488 if ( $post_tags ) {
489 foreach ( $post_tags as $post_tag ) {
490 $tag = array(
491 'type' => 'Hashtag',
492 'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ),
493 'name' => esc_hashtag( $post_tag->name ),
494 );
495 $tags[] = $tag;
496 }
497 }
498
499 $mentions = $this->get_mentions();
500 if ( $mentions ) {
501 foreach ( $mentions as $mention => $url ) {
502 $tag = array(
503 'type' => 'Mention',
504 'href' => \esc_url( $url ),
505 'name' => \esc_html( $mention ),
506 );
507 $tags[] = $tag;
508 }
509 }
510
511 return $tags;
512 }
513
514 /**
515 * Returns the content for the ActivityPub Item.
516 *
517 * The content will be generated based on the user settings.
518 *
519 * @return string The content.
520 */
521 protected function get_content() {
522 global $post;
523
524 /**
525 * Provides an action hook so plugins can add their own hooks/filters before AP content is generated.
526 *
527 * Example: if a plugin adds a filter to `the_content` to add a button to the end of posts, it can also remove that filter here.
528 *
529 * @param WP_Post $post The post object.
530 */
531 do_action( 'activitypub_before_get_content', $post );
532
533 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
534 $post = $this->wp_post;
535 $content = $this->get_post_content_template();
536
537 // Register our shortcodes just in time.
538 Shortcodes::register();
539 // Fill in the shortcodes.
540 setup_postdata( $post );
541 $content = do_shortcode( $content );
542 wp_reset_postdata();
543
544 $content = \wpautop( $content );
545 $content = \preg_replace( '/[\n\r\t]/', '', $content );
546 $content = \trim( $content );
547
548 $content = \apply_filters( 'activitypub_the_content', $content, $post );
549
550 // Don't need these any more, should never appear in a post.
551 Shortcodes::unregister();
552
553 return $content;
554 }
555
556 /**
557 * Gets the template to use to generate the content of the activitypub item.
558 *
559 * @return string The Template.
560 */
561 protected function get_post_content_template() {
562 if ( 'excerpt' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
563 return "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
564 }
565
566 if ( 'title' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
567 return "[ap_title]\n\n[ap_permalink type=\"html\"]";
568 }
569
570 if ( 'content' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
571 return "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
572 }
573
574 return \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
575 }
576
577 /**
578 * Helper function to get the @-Mentions from the post content.
579 *
580 * @return array The list of @-Mentions.
581 */
582 protected function get_mentions() {
583 return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_post->post_content, $this->wp_post );
584 }
585
586 /**
587 * Returns the locale of the post.
588 *
589 * @return string The locale of the post.
590 */
591 public function get_locale() {
592 $post_id = $this->wp_post->ID;
593 $lang = \strtolower( \strtok( \get_locale(), '_-' ) );
594
595 /**
596 * Filter the locale of the post.
597 *
598 * @param string $lang The locale of the post.
599 * @param int $post_id The post ID.
600 * @param WP_Post $post The post object.
601 *
602 * @return string The filtered locale of the post.
603 */
604 return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->wp_post );
605 }
606 }
607