PluginProbe
ActivityPub / 8.0.2
ActivityPub v8.0.2
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-base.php +559 -32 2.0.18.0.2 View file →
@@ -1,18 +1,32 @@
1 1 <?php
2 +/**
3 + * Base Transformer Class file.
4 + *
5 + * @package Activitypub
6 + */
7 +
2 8 namespace Activitypub\Transformer;
3 9
4 -use WP_Post;
5 -use WP_Comment;
6 -
7 10 use Activitypub\Activity\Activity;
8 11 use Activitypub\Activity\Base_Object;
12 +use Activitypub\Collection\Actors;
13 +use Activitypub\Http;
9 14
15 +use function Activitypub\get_upload_baseurl;
16 +use function Activitypub\object_to_uri;
17 +
10 18 /**
11 - * WordPress Base Transformer
19 + * WordPress Base Transformer.
12 20 *
13 - * Transformers are responsible for transforming a WordPress objects into different ActivityPub
21 + * Transformers are responsible for transforming WordPress objects into different ActivityPub
14 22 * Object-Types or Activities.
23 + *
24 + * @method string|null get_content() Returns the content for the transformed item.
25 + * @method string|array|null get_icon() Returns an icon for the transformed item.
26 + * @method string|null get_id() Returns the ID for the transformed item.
27 + * @method string|null get_name() Returns the name for the transformed item.
28 + * @method string|null get_summary() Returns the summary for the transformed item.
15 29 */
16 30 abstract class Base {
17 31 /**
18 32 * The WP_Post or WP_Comment object.
@@ -18,67 +32,221 @@
18 32 * The WP_Post or WP_Comment object.
19 33 *
20 34 * This is the source object of the transformer.
21 35 *
22 - * @var WP_Post|WP_Comment
36 + * @var \WP_Post|\WP_Comment|Base_Object|string|array|\WP_Term
23 37 */
38 + protected $item;
39 +
40 + /**
41 + * The WP_Post or WP_Comment object.
42 + *
43 + * @deprecated version 5.0.0
44 + *
45 + * @var \WP_Post|\WP_Comment
46 + */
24 47 protected $wp_object;
25 48
26 49 /**
50 + * The content visibility.
51 + *
52 + * @var string
53 + */
54 + protected $content_visibility;
55 +
56 + /**
27 57 * Static function to Transform a WordPress Object.
28 58 *
29 59 * This helps to chain the output of the Transformer.
30 60 *
31 - * @param WP_Post|WP_Comment $wp_object The WordPress object
61 + * @param \WP_Post|\WP_Comment|Base_Object|string|array|\WP_term $item The item that should be transformed.
32 62 *
33 - * @return void
63 + * @return Base
34 64 */
35 - public static function transform( $object ) {
36 - return new static( $object );
65 + public static function transform( $item ) {
66 + return new static( $item );
37 67 }
38 68
39 69 /**
40 70 * Base constructor.
41 71 *
42 - * @param WP_Post|WP_Comment $wp_object The WordPress object
72 + * @param \WP_Post|\WP_Comment|Base_Object|string|array|\WP_Term $item The item that should be transformed.
43 73 */
44 - public function __construct( $wp_object ) {
45 - $this->wp_object = $wp_object;
74 + public function __construct( $item ) {
75 + $this->item = $item;
76 + $this->wp_object = $item;
46 77 }
47 78
48 79 /**
49 - * Transform the WordPress Object into an ActivityPub Object.
80 + * Transform all properties with available get(ter) functions.
50 81 *
51 - * @return Activitypub\Activity\Base_Object
82 + * @param Base_Object $activity_object The ActivityPub Object.
83 + *
84 + * @return Base_Object|\WP_Error The transformed ActivityPub Object or WP_Error on failure.
52 85 */
53 - public function to_object() {
54 - $activitypub_object = new Base_Object();
86 + protected function transform_object_properties( $activity_object ) {
87 + if ( ! $activity_object || \is_wp_error( $activity_object ) ) {
88 + return $activity_object;
89 + }
55 90
56 - $vars = $activitypub_object->get_object_var_keys();
91 + // Save activity in the context of an activitypub request.
92 + \add_filter( 'activitypub_is_activitypub_request', '__return_true' );
57 93
94 + $vars = $activity_object->get_object_var_keys();
95 +
58 96 foreach ( $vars as $var ) {
59 97 $getter = 'get_' . $var;
60 98
61 - if ( method_exists( $this, $getter ) ) {
62 - $value = call_user_func( array( $this, $getter ) );
99 + if ( \method_exists( $this, $getter ) ) {
100 + $value = \call_user_func( array( $this, $getter ) );
63 101
64 - if ( isset( $value ) ) {
102 + if ( null !== $value ) {
65 103 $setter = 'set_' . $var;
66 104
67 - call_user_func( array( $activitypub_object, $setter ), $value );
105 + /**
106 + * Filter the value before it is set to the Activity-Object `$activity_object`.
107 + *
108 + * @param mixed $value The value that should be set.
109 + * @param mixed $item The Object.
110 + */
111 + $value = \apply_filters( "activitypub_transform_{$setter}", $value, $this->item );
112 +
113 + /**
114 + * Filter the value before it is set to the Activity-Object `$activity_object`.
115 + *
116 + * @param mixed $value The value that should be set.
117 + * @param string $var The variable name.
118 + * @param mixed $item The Object.
119 + */
120 + $value = \apply_filters( 'activitypub_transform_set', $value, $var, $this->item );
121 +
122 + \call_user_func( array( $activity_object, $setter ), $value );
68 123 }
69 124 }
70 125 }
71 126
72 - return $activitypub_object;
127 + // Remove activity in the context of an activitypub request.
128 + \remove_filter( 'activitypub_is_activitypub_request', '__return_true' );
129 +
130 + return $activity_object;
73 131 }
74 132
75 133 /**
134 + * Transform the item into an ActivityPub Object.
135 + *
136 + * @return Base_Object The Activity-Object.
137 + */
138 + public function to_object() {
139 + $activity_object = new Base_Object();
140 + $activity_object = $this->transform_object_properties( $activity_object );
141 +
142 + if ( \is_wp_error( $activity_object ) ) {
143 + return $activity_object;
144 + }
145 +
146 + return $this->set_audience( $activity_object );
147 + }
148 +
149 + /**
150 + * Get the content visibility.
151 + *
152 + * @return string The content visibility.
153 + */
154 + public function get_content_visibility() {
155 + if ( ! $this->content_visibility ) {
156 + return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC;
157 + }
158 +
159 + return $this->content_visibility;
160 + }
161 +
162 + /**
163 + * Set the content visibility.
164 + *
165 + * @param string $content_visibility The content visibility.
166 + */
167 + public function set_content_visibility( $content_visibility ) {
168 + $this->content_visibility = $content_visibility;
169 +
170 + return $this;
171 + }
172 +
173 + /**
174 + * Set the audience.
175 + *
176 + * @param Base_Object $activity_object The ActivityPub Object.
177 + *
178 + * @return Base_Object The ActivityPub Object.
179 + */
180 + protected function set_audience( $activity_object ) {
181 + $public = 'https://www.w3.org/ns/activitystreams#Public';
182 + $followers = null;
183 + $replied_to = null;
184 +
185 + $actor = Actors::get_by_resource( $this->get_attributed_to() );
186 + if ( ! \is_wp_error( $actor ) ) {
187 + $followers = $actor->get_followers();
188 + }
189 +
190 + $mentions = array_values( $this->get_mentions() );
191 +
192 + if ( $this->get_in_reply_to() ) {
193 + $object = Http::get_remote_object( $this->get_in_reply_to() );
194 + if ( $object && ! \is_wp_error( $object ) && isset( $object['attributedTo'] ) ) {
195 + $replied_to = array( object_to_uri( $object['attributedTo'] ) );
196 + }
197 + }
198 +
199 + switch ( $this->get_content_visibility() ) {
200 + case ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC:
201 + $activity_object->add_to( $public );
202 + $activity_object->add_cc( $followers );
203 + $activity_object->add_cc( $mentions );
204 + $activity_object->add_cc( $replied_to );
205 + break;
206 + case ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC:
207 + $activity_object->add_to( $followers );
208 + $activity_object->add_to( $mentions );
209 + $activity_object->add_to( $replied_to );
210 + $activity_object->add_cc( $public );
211 + break;
212 + case ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE:
213 + $activity_object->add_to( $mentions );
214 + $activity_object->add_to( $replied_to );
215 + }
216 +
217 + return $activity_object;
218 + }
219 +
220 + /**
221 + * Transform the item to an ActivityPub ID.
222 + *
223 + * @return string The ID of the WordPress Object.
224 + */
225 + public function to_id() {
226 + /* @var Attachment|Comment|Json|Post|User $this Object transformer. */
227 + return $this->get_id();
228 + }
229 +
230 + /**
231 + * Returns a Tombstone object for the item.
232 + *
233 + * @return Base_Object The Tombstone object.
234 + */
235 + public function to_tombstone() {
236 + $object = new Base_Object();
237 + $object->set_type( 'Tombstone' );
238 + $object->set_id( $this->to_id() );
239 +
240 + return $object;
241 + }
242 +
243 + /**
76 244 * Transforms the ActivityPub Object to an Activity
77 245 *
78 246 * @param string $type The Activity-Type.
79 247 *
80 - * @return \Activitypub\Activity\Activity The Activity.
248 + * @return Activity The Activity.
81 249 */
82 250 public function to_activity( $type ) {
83 251 $object = $this->to_object();
84 252
@@ -83,12 +251,14 @@
83 251 $object = $this->to_object();
84 252
85 253 $activity = new Activity();
86 254 $activity->set_type( $type );
255 +
256 + // Pre-fill the Activity with data (for example, cc and to).
87 257 $activity->set_object( $object );
88 258
89 - // Use simple Object (only ID-URI) for Like and Announce
90 - if ( in_array( $type, array( 'Like', 'Announce' ), true ) ) {
259 + // Use simple Object (only ID-URI) for Like and Announce.
260 + if ( 'Like' === $type ) {
91 261 $activity->set_object( $object->get_id() );
92 262 }
93 263
94 264 return $activity;
@@ -94,17 +264,374 @@
94 264 return $activity;
95 265 }
96 266
97 267 /**
98 - * Returns the ID of the WordPress Object.
268 + * Returns a generic locale based on the Blog settings.
99 269 *
100 - * @return int The ID of the WordPress Object
270 + * @return string The locale of the blog.
101 271 */
102 - abstract public function get_wp_user_id();
272 + protected function get_locale() {
273 + $lang = \strtolower( \strtok( \get_locale(), '_-' ) );
103 274
275 + /**
276 + * Filter the locale of the post.
277 + *
278 + * @param string $lang The locale of the post.
279 + * @param mixed $item The post object.
280 + *
281 + * @return string The filtered locale of the post.
282 + */
283 + return apply_filters( 'activitypub_locale', $lang, $this->item );
284 + }
285 +
104 286 /**
105 - * Change the User-ID of the WordPress Post.
287 + * Returns the default media type for an Object.
106 288 *
107 - * @return int The User-ID of the WordPress Post
289 + * @return string The media type.
108 290 */
109 - abstract public function change_wp_user_id( $user_id );
291 + public function get_media_type() {
292 + return 'text/html';
293 + }
294 +
295 + /**
296 + * Returns the content map for the post.
297 + *
298 + * @return array|null The content map for the post or null if not set.
299 + */
300 + protected function get_content_map() {
301 + if ( ! \method_exists( $this, 'get_content' ) || ! $this->get_content() ) {
302 + return null;
303 + }
304 +
305 + return array(
306 + $this->get_locale() => $this->get_content(),
307 + );
308 + }
309 +
310 + /**
311 + * Returns the name map for the post.
312 + *
313 + * @return array|null The name map for the post or null if not set.
314 + */
315 + protected function get_name_map() {
316 + if ( ! \method_exists( $this, 'get_name' ) || ! $this->get_name() ) {
317 + return null;
318 + }
319 +
320 + return array(
321 + $this->get_locale() => $this->get_name(),
322 + );
323 + }
324 +
325 + /**
326 + * Returns the summary map for the post.
327 + *
328 + * @return array|null The summary map for the post or null if not set.
329 + */
330 + protected function get_summary_map() {
331 + if ( ! \method_exists( $this, 'get_summary' ) || ! $this->get_summary() ) {
332 + return null;
333 + }
334 +
335 + return array(
336 + $this->get_locale() => $this->get_summary(),
337 + );
338 + }
339 +
340 + /**
341 + * Returns the tags for the post.
342 + *
343 + * @return array The tags for the post.
344 + */
345 + protected function get_tag() {
346 + $tags = array();
347 + $mentions = $this->get_mentions();
348 +
349 + foreach ( $mentions as $mention => $url ) {
350 + $tags[] = array(
351 + 'type' => 'Mention',
352 + 'href' => \esc_url( $url ),
353 + 'name' => \esc_html( $mention ),
354 + );
355 + }
356 +
357 + return \array_unique( $tags, SORT_REGULAR );
358 + }
359 +
360 + /**
361 + * Get the attributed to.
362 + *
363 + * @return string The attributed to.
364 + */
365 + protected function get_attributed_to() {
366 + return null;
367 + }
368 +
369 + /**
370 + * Extracts mentions from the content.
371 + *
372 + * @return array The mentions.
373 + */
374 + protected function get_mentions() {
375 + $content = '';
376 +
377 + if ( method_exists( $this, 'get_content' ) ) {
378 + $content = $content . ' ' . $this->get_content();
379 + }
380 +
381 + if ( method_exists( $this, 'get_summary' ) ) {
382 + $content = $content . ' ' . $this->get_summary();
383 + }
384 +
385 + /**
386 + * Filter the mentions in the post content.
387 + *
388 + * @param array $mentions The mentions.
389 + * @param string $content The post content.
390 + * @param \WP_Post $post The post object.
391 + *
392 + * @return array The filtered mentions.
393 + */
394 + return apply_filters(
395 + 'activitypub_extract_mentions',
396 + array(),
397 + $content,
398 + $this->item
399 + );
400 + }
401 +
402 + /**
403 + * Returns the in reply to.
404 + *
405 + * @return string|array|null The in reply to.
406 + */
407 + protected function get_in_reply_to() {
408 + return null;
409 + }
410 +
411 + /**
412 + * Parse HTML content for image tags and extract attachment information.
413 + *
414 + * This method is used by both Post and Comment transformers to find images
415 + * embedded in HTML content and extract their attachment IDs and alt text.
416 + *
417 + * @param array $media The existing media array grouped by type.
418 + * @param int $max_images Maximum number of images to extract.
419 + * @param string $content The HTML content to parse.
420 + *
421 + * @return array The updated media array with found images.
422 + */
423 + protected function parse_html_images( $media, $max_images, $content ) {
424 + // If someone calls that function directly, bail.
425 + if ( ! \class_exists( '\WP_HTML_Tag_Processor' ) ) {
426 + return $media;
427 + }
428 +
429 + // Max images can't be negative or zero.
430 + if ( $max_images <= 0 ) {
431 + return $media;
432 + }
433 +
434 + $images = array();
435 + $base = get_upload_baseurl();
436 + $tags = new \WP_HTML_Tag_Processor( $content );
437 +
438 + // This linter warning is a false positive - we have to re-count each time here as we modify $images.
439 + // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
440 + while ( $tags->next_tag( 'img' ) && ( \count( $images ) <= $max_images ) ) {
441 + /**
442 + * Filter the image source URL.
443 + *
444 + * This can be used to modify the image source URL before it is used to
445 + * determine the attachment ID.
446 + *
447 + * @param string $src The image source URL.
448 + */
449 + $src = \apply_filters( 'activitypub_image_src', $tags->get_attribute( 'src' ) );
450 +
451 + /*
452 + * If the img source is in our uploads dir, get the
453 + * associated ID. Note: if there's a -500x500
454 + * type suffix, we remove it, but we try the original
455 + * first in case the original image is actually called
456 + * that. Likewise, we try adding the -scaled suffix for
457 + * the case that this is a small version of an image
458 + * that was big enough to get scaled down on upload:
459 + * https://make.wordpress.org/core/2019/10/09/introducing-handling-of-big-images-in-wordpress-5-3/
460 + */
461 + if ( null !== $src && \str_starts_with( $src, $base ) ) {
462 + $img_id = \attachment_url_to_postid( $src );
463 +
464 + if ( 0 === $img_id ) {
465 + $count = 0;
466 + $src = \strtok( $src, '?' );
467 + $img_id = \attachment_url_to_postid( $src );
468 + }
469 +
470 + if ( 0 === $img_id ) {
471 + $count = 0;
472 + $src = \preg_replace( '/-(?:\d+x\d+)(\.[a-zA-Z]+)$/', '$1', $src, 1, $count );
473 + if ( $count > 0 ) {
474 + $img_id = \attachment_url_to_postid( $src );
475 + }
476 + }
477 +
478 + if ( 0 === $img_id ) {
479 + $src = \preg_replace( '/(\.[a-zA-Z]+)$/', '-scaled$1', $src );
480 + $img_id = \attachment_url_to_postid( $src );
481 + }
482 +
483 + if ( 0 !== $img_id ) {
484 + $images[] = array(
485 + 'id' => $img_id,
486 + 'alt' => $tags->get_attribute( 'alt' ),
487 + );
488 + }
489 + }
490 + }
491 +
492 + if ( \count( $media['image'] ) <= $max_images ) {
493 + $media['image'] = \array_merge( $media['image'], $images );
494 + }
495 +
496 + return $media;
497 + }
498 +
499 + /**
500 + * Transforms a WordPress attachment array to ActivityStreams attachment format.
501 + *
502 + * @param array $media The WordPress attachment array with 'id', optional 'alt', and optional 'icon'.
503 + *
504 + * @return array The ActivityStreams attachment array.
505 + */
506 + protected function transform_attachment( $media ) {
507 + if ( ! isset( $media['id'] ) ) {
508 + return $media;
509 + }
510 +
511 + $id = $media['id'];
512 + $attachment = array();
513 + $mime_type = \get_post_mime_type( $id );
514 + $media_type = \strtok( $mime_type, '/' );
515 +
516 + // Switching on image/audio/video.
517 + switch ( $media_type ) {
518 + case 'image':
519 + $image_size = 'large';
520 +
521 + /**
522 + * Filter the image URL returned for each post.
523 + *
524 + * @param array|false $thumbnail The image URL, or false if no image is available.
525 + * @param int $id The attachment ID.
526 + * @param string $image_size The image size to retrieve. Set to 'large' by default.
527 + */
528 + $thumbnail = \apply_filters( 'activitypub_get_image', $this->get_attachment_image_src( $id, $image_size ), $id, $image_size );
529 +
530 + if ( $thumbnail ) {
531 + $image = array(
532 + 'type' => 'Image',
533 + 'url' => \esc_url( $thumbnail[0] ),
534 + 'mediaType' => \esc_attr( $mime_type ),
535 + );
536 +
537 + if ( ! empty( $media['alt'] ) ) {
538 + $image['name'] = \html_entity_decode( \wp_strip_all_tags( $media['alt'] ), ENT_QUOTES, 'UTF-8' );
539 + } else {
540 + $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
541 + if ( $alt ) {
542 + $image['name'] = \html_entity_decode( \wp_strip_all_tags( $alt ), ENT_QUOTES, 'UTF-8' );
543 + }
544 + }
545 +
546 + $attachment = $image;
547 + }
548 + break;
549 +
550 + case 'audio':
551 + case 'video':
552 + $meta = \wp_get_attachment_metadata( $id );
553 + $attachment = array(
554 + 'type' => \ucfirst( $media_type ),
555 + 'mediaType' => \esc_attr( $mime_type ),
556 + 'url' => \esc_url( \wp_get_attachment_url( $id ) ),
557 + 'name' => \esc_attr( \get_the_title( $id ) ),
558 + );
559 +
560 + // Height and width for videos.
561 + if ( isset( $meta['width'], $meta['height'] ) ) {
562 + $attachment['width'] = \esc_attr( $meta['width'] );
563 + $attachment['height'] = \esc_attr( $meta['height'] );
564 + }
565 +
566 + // Use poster image from the block, or fall back to the transformer icon.
567 + if ( ! empty( $media['icon'] ) ) {
568 + $attachment['icon'] = \esc_url_raw( $media['icon'] );
569 + } elseif ( \method_exists( $this, 'get_icon' ) && $this->get_icon() ) {
570 + $attachment['icon'] = object_to_uri( $this->get_icon() );
571 + }
572 + break;
573 + }
574 +
575 + /**
576 + * Filter the attachment for a post.
577 + *
578 + * @param array $attachment The attachment.
579 + * @param int $id The attachment ID.
580 + *
581 + * @return array The filtered attachment.
582 + */
583 + return \apply_filters( 'activitypub_attachment', $attachment, $id );
584 + }
585 +
586 + /**
587 + * Return details about an image attachment.
588 + *
589 + * @param int $id The attachment ID.
590 + * @param string $image_size The image size to retrieve. Set to 'large' by default.
591 + *
592 + * @return array|false Array of image data, or boolean false if no image is available.
593 + */
594 + protected function get_attachment_image_src( $id, $image_size = 'large' ) {
595 + /**
596 + * Hook into the image retrieval process. Before image retrieval.
597 + *
598 + * @param int $id The attachment ID.
599 + * @param string $image_size The image size to retrieve. Set to 'large' by default.
600 + */
601 + \do_action( 'activitypub_get_image_pre', $id, $image_size );
602 +
603 + $image = \wp_get_attachment_image_src( $id, $image_size );
604 +
605 + /**
606 + * Hook into the image retrieval process. After image retrieval.
607 + *
608 + * @param int $id The attachment ID.
609 + * @param string $image_size The image size to retrieve. Set to 'large' by default.
610 + */
611 + \do_action( 'activitypub_get_image_post', $id, $image_size );
612 +
613 + return $image;
614 + }
615 +
616 + /**
617 + * Filter attachments to ensure uniqueness based on their ID.
618 + *
619 + * @param array $attachments Array of attachments with 'id' field.
620 + *
621 + * @return array Array with duplicate attachments removed.
622 + */
623 + protected function filter_unique_attachments( $attachments ) {
624 + $seen_ids = array();
625 +
626 + return \array_filter(
627 + $attachments,
628 + static function ( $attachment ) use ( &$seen_ids ) {
629 + if ( isset( $attachment['id'] ) && ! in_array( $attachment['id'], $seen_ids, true ) ) {
630 + $seen_ids[] = $attachment['id'];
631 + return true;
632 + }
633 + return false;
634 + }
635 + );
636 + }
110 637 }