PluginProbe
ActivityPub / 1.1.0
ActivityPub v1.1.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.1.0, at includes/transformer/class-post.php

605 lines 15.7 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 $media_ids = \array_unique( $media_ids );
189
190 return \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $media_ids ) );
191 }
192
193 /**
194 * Get image attachments from the classic editor.
195 * Note that audio/video attachments are only supported in the block editor.
196 *
197 * @param int $max_images The maximum number of images to return.
198 *
199 * @return array The attachments.
200 */
201 protected function get_classic_editor_images( $max_images ) {
202 // max images can't be negative or zero
203 if ( $max_images <= 0 ) {
204 return array();
205 }
206
207 $id = $this->wp_post->ID;
208
209 $image_ids = array();
210
211 // list post thumbnail first if this post has one
212 if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
213 $image_ids[] = \get_post_thumbnail_id( $id );
214 --$max_images;
215 }
216
217 if ( $max_images > 0 ) {
218 $query = new \WP_Query(
219 array(
220 'post_parent' => $id,
221 'post_status' => 'inherit',
222 'post_type' => 'attachment',
223 'post_mime_type' => 'image',
224 'order' => 'ASC',
225 'orderby' => 'menu_order ID',
226 'posts_per_page' => $max_images,
227 )
228 );
229 foreach ( $query->get_posts() as $attachment ) {
230 if ( ! \in_array( $attachment->ID, $image_ids, true ) ) {
231 $image_ids[] = $attachment->ID;
232 }
233 }
234 }
235 $image_ids = \array_unique( $image_ids );
236
237 return \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $image_ids ) );
238 }
239
240 /**
241 * Recursively get media IDs from blocks.
242 * @param array $blocks The blocks to search for media IDs
243 * @param array $media_ids The media IDs to append new IDs to
244 * @param int $max_media The maximum number of media to return.
245 *
246 * @return array The image IDs.
247 */
248 protected static function get_media_ids_from_blocks( $blocks, $media_ids, $max_media ) {
249
250 foreach ( $blocks as $block ) {
251 // recurse into inner blocks
252 if ( ! empty( $block['innerBlocks'] ) ) {
253 $media_ids = self::get_media_ids_from_blocks( $block['innerBlocks'], $media_ids, $max_media );
254 }
255
256 switch ( $block['blockName'] ) {
257 case 'core/image':
258 case 'core/cover':
259 case 'core/audio':
260 case 'core/video':
261 case 'videopress/video':
262 if ( ! empty( $block['attrs']['id'] ) ) {
263 $media_ids[] = $block['attrs']['id'];
264 }
265 break;
266 case 'jetpack/slideshow':
267 case 'jetpack/tiled-gallery':
268 if ( ! empty( $block['attrs']['ids'] ) ) {
269 $media_ids = array_merge( $media_ids, $block['attrs']['ids'] );
270 }
271 break;
272 case 'jetpack/image-compare':
273 if ( ! empty( $block['attrs']['beforeImageId'] ) ) {
274 $media_ids[] = $block['attrs']['beforeImageId'];
275 }
276 if ( ! empty( $block['attrs']['afterImageId'] ) ) {
277 $media_ids[] = $block['attrs']['afterImageId'];
278 }
279 break;
280 }
281
282 // stop doing unneeded work
283 if ( count( $media_ids ) >= $max_media ) {
284 break;
285 }
286 }
287
288 // still need to slice it because one gallery could knock us over the limit
289 return array_slice( $media_ids, 0, $max_media );
290 }
291
292 /**
293 * Converts a WordPress Attachment to an ActivityPub Attachment.
294 *
295 * @param int $id The Attachment ID.
296 *
297 * @return array The ActivityPub Attachment.
298 */
299 public static function wp_attachment_to_activity_attachment( $id ) {
300 $attachment = array();
301 $mime_type = \get_post_mime_type( $id );
302 $mime_type_parts = \explode( '/', $mime_type );
303 // switching on image/audio/video
304 switch ( $mime_type_parts[0] ) {
305 case 'image':
306 $image_size = 'full';
307
308 /**
309 * Filter the image URL returned for each post.
310 *
311 * @param array|false $thumbnail The image URL, or false if no image is available.
312 * @param int $id The attachment ID.
313 * @param string $image_size The image size to retrieve. Set to 'full' by default.
314 */
315 $thumbnail = apply_filters(
316 'activitypub_get_image',
317 self::get_image( $id, $image_size ),
318 $id,
319 $image_size
320 );
321
322 if ( $thumbnail ) {
323 $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
324 $image = array(
325 'type' => 'Image',
326 'url' => $thumbnail[0],
327 'mediaType' => $mime_type,
328 );
329
330 if ( $alt ) {
331 $image['name'] = $alt;
332 }
333 $attachment = $image;
334 }
335 break;
336
337 case 'audio':
338 case 'video':
339 $attachment = array(
340 'type' => 'Document',
341 'mediaType' => $mime_type,
342 'url' => \wp_get_attachment_url( $id ),
343 'name' => \get_the_title( $id ),
344 );
345 $meta = wp_get_attachment_metadata( $id );
346 // height and width for videos
347 if ( isset( $meta['width'] ) && isset( $meta['height'] ) ) {
348 $attachment['width'] = $meta['width'];
349 $attachment['height'] = $meta['height'];
350 }
351 // @todo: add `icon` support for audio/video attachments. Maybe use post thumbnail?
352 break;
353 }
354
355 return \apply_filters( 'activitypub_attachment', $attachment, $id );
356 }
357
358 /**
359 * Return details about an image attachment.
360 *
361 * @param int $id The attachment ID.
362 * @param string $image_size The image size to retrieve. Set to 'full' by default.
363 *
364 * @return array|false Array of image data, or boolean false if no image is available.
365 */
366 protected static function get_image( $id, $image_size = 'full' ) {
367 /**
368 * Hook into the image retrieval process. Before image retrieval.
369 *
370 * @param int $id The attachment ID.
371 * @param string $image_size The image size to retrieve. Set to 'full' by default.
372 */
373 do_action( 'activitypub_get_image_pre', $id, $image_size );
374
375 $image = \wp_get_attachment_image_src( $id, $image_size );
376
377 /**
378 * Hook into the image retrieval process. After image retrieval.
379 *
380 * @param int $id The attachment ID.
381 * @param string $image_size The image size to retrieve. Set to 'full' by default.
382 */
383 do_action( 'activitypub_get_image_post', $id, $image_size );
384
385 return $image;
386 }
387
388 /**
389 * Returns the ActivityStreams 2.0 Object-Type for a Post based on the
390 * settings and the Post-Type.
391 *
392 * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
393 *
394 * @return string The Object-Type.
395 */
396 protected function get_object_type() {
397 if ( 'wordpress-post-format' !== \get_option( 'activitypub_object_type', 'note' ) ) {
398 return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) );
399 }
400
401 // Default to Article.
402 $object_type = 'Article';
403 $post_type = \get_post_type( $this->wp_post );
404 switch ( $post_type ) {
405 case 'post':
406 $post_format = \get_post_format( $this->wp_post );
407 switch ( $post_format ) {
408 case 'aside':
409 case 'status':
410 case 'quote':
411 case 'note':
412 $object_type = 'Note';
413 break;
414 case 'gallery':
415 case 'image':
416 $object_type = 'Image';
417 break;
418 case 'video':
419 $object_type = 'Video';
420 break;
421 case 'audio':
422 $object_type = 'Audio';
423 break;
424 default:
425 $object_type = 'Article';
426 break;
427 }
428 break;
429 case 'page':
430 $object_type = 'Page';
431 break;
432 case 'attachment':
433 $mime_type = \get_post_mime_type();
434 $media_type = \preg_replace( '/(\/[a-zA-Z]+)/i', '', $mime_type );
435 switch ( $media_type ) {
436 case 'audio':
437 $object_type = 'Audio';
438 break;
439 case 'video':
440 $object_type = 'Video';
441 break;
442 case 'image':
443 $object_type = 'Image';
444 break;
445 }
446 break;
447 default:
448 $object_type = 'Article';
449 break;
450 }
451
452 return $object_type;
453 }
454
455 /**
456 * Returns a list of Mentions, used in the Post.
457 *
458 * @see https://docs.joinmastodon.org/spec/activitypub/#Mention
459 *
460 * @return array The list of Mentions.
461 */
462 protected function get_cc() {
463 $cc = array();
464
465 $mentions = $this->get_mentions();
466 if ( $mentions ) {
467 foreach ( $mentions as $url ) {
468 $cc[] = $url;
469 }
470 }
471
472 return $cc;
473 }
474
475 /**
476 * Returns a list of Tags, used in the Post.
477 *
478 * This includes Hash-Tags and Mentions.
479 *
480 * @return array The list of Tags.
481 */
482 protected function get_tags() {
483 $tags = array();
484
485 $post_tags = \get_the_tags( $this->wp_post->ID );
486 if ( $post_tags ) {
487 foreach ( $post_tags as $post_tag ) {
488 $tag = array(
489 'type' => 'Hashtag',
490 'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ),
491 'name' => esc_hashtag( $post_tag->name ),
492 );
493 $tags[] = $tag;
494 }
495 }
496
497 $mentions = $this->get_mentions();
498 if ( $mentions ) {
499 foreach ( $mentions as $mention => $url ) {
500 $tag = array(
501 'type' => 'Mention',
502 'href' => \esc_url( $url ),
503 'name' => \esc_html( $mention ),
504 );
505 $tags[] = $tag;
506 }
507 }
508
509 return $tags;
510 }
511
512 /**
513 * Returns the content for the ActivityPub Item.
514 *
515 * The content will be generated based on the user settings.
516 *
517 * @return string The content.
518 */
519 protected function get_content() {
520 global $post;
521
522 /**
523 * Provides an action hook so plugins can add their own hooks/filters before AP content is generated.
524 *
525 * 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.
526 *
527 * @param WP_Post $post The post object.
528 */
529 do_action( 'activitypub_before_get_content', $post );
530
531 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
532 $post = $this->wp_post;
533 $content = $this->get_post_content_template();
534
535 // Register our shortcodes just in time.
536 Shortcodes::register();
537 // Fill in the shortcodes.
538 setup_postdata( $post );
539 $content = do_shortcode( $content );
540 wp_reset_postdata();
541
542 $content = \wpautop( $content );
543 $content = \preg_replace( '/[\n\r\t]/', '', $content );
544 $content = \trim( $content );
545
546 $content = \apply_filters( 'activitypub_the_content', $content, $post );
547
548 // Don't need these any more, should never appear in a post.
549 Shortcodes::unregister();
550
551 return $content;
552 }
553
554 /**
555 * Gets the template to use to generate the content of the activitypub item.
556 *
557 * @return string The Template.
558 */
559 protected function get_post_content_template() {
560 if ( 'excerpt' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
561 return "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
562 }
563
564 if ( 'title' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
565 return "[ap_title]\n\n[ap_permalink type=\"html\"]";
566 }
567
568 if ( 'content' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
569 return "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
570 }
571
572 return \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
573 }
574
575 /**
576 * Helper function to get the @-Mentions from the post content.
577 *
578 * @return array The list of @-Mentions.
579 */
580 protected function get_mentions() {
581 return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_post->post_content, $this->wp_post );
582 }
583
584 /**
585 * Returns the locale of the post.
586 *
587 * @return string The locale of the post.
588 */
589 public function get_locale() {
590 $post_id = $this->wp_post->ID;
591 $lang = \strtolower( \strtok( \get_locale(), '_-' ) );
592
593 /**
594 * Filter the locale of the post.
595 *
596 * @param string $lang The locale of the post.
597 * @param int $post_id The post ID.
598 * @param WP_Post $post The post object.
599 *
600 * @return string The filtered locale of the post.
601 */
602 return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->wp_post );
603 }
604 }
605