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
activitypub / includes / transformer / class-base.php

class-base.php in ActivityPub 8.0.2, at includes/transformer/class-base.php

638 lines 17.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Base Transformer Class file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Transformer;
9
10 use Activitypub\Activity\Activity;
11 use Activitypub\Activity\Base_Object;
12 use Activitypub\Collection\Actors;
13 use Activitypub\Http;
14
15 use function Activitypub\get_upload_baseurl;
16 use function Activitypub\object_to_uri;
17
18 /**
19 * WordPress Base Transformer.
20 *
21 * Transformers are responsible for transforming WordPress objects into different ActivityPub
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.
29 */
30 abstract class Base {
31 /**
32 * The WP_Post or WP_Comment object.
33 *
34 * This is the source object of the transformer.
35 *
36 * @var \WP_Post|\WP_Comment|Base_Object|string|array|\WP_Term
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 */
47 protected $wp_object;
48
49 /**
50 * The content visibility.
51 *
52 * @var string
53 */
54 protected $content_visibility;
55
56 /**
57 * Static function to Transform a WordPress Object.
58 *
59 * This helps to chain the output of the Transformer.
60 *
61 * @param \WP_Post|\WP_Comment|Base_Object|string|array|\WP_term $item The item that should be transformed.
62 *
63 * @return Base
64 */
65 public static function transform( $item ) {
66 return new static( $item );
67 }
68
69 /**
70 * Base constructor.
71 *
72 * @param \WP_Post|\WP_Comment|Base_Object|string|array|\WP_Term $item The item that should be transformed.
73 */
74 public function __construct( $item ) {
75 $this->item = $item;
76 $this->wp_object = $item;
77 }
78
79 /**
80 * Transform all properties with available get(ter) functions.
81 *
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.
85 */
86 protected function transform_object_properties( $activity_object ) {
87 if ( ! $activity_object || \is_wp_error( $activity_object ) ) {
88 return $activity_object;
89 }
90
91 // Save activity in the context of an activitypub request.
92 \add_filter( 'activitypub_is_activitypub_request', '__return_true' );
93
94 $vars = $activity_object->get_object_var_keys();
95
96 foreach ( $vars as $var ) {
97 $getter = 'get_' . $var;
98
99 if ( \method_exists( $this, $getter ) ) {
100 $value = \call_user_func( array( $this, $getter ) );
101
102 if ( null !== $value ) {
103 $setter = 'set_' . $var;
104
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 );
123 }
124 }
125 }
126
127 // Remove activity in the context of an activitypub request.
128 \remove_filter( 'activitypub_is_activitypub_request', '__return_true' );
129
130 return $activity_object;
131 }
132
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 /**
244 * Transforms the ActivityPub Object to an Activity
245 *
246 * @param string $type The Activity-Type.
247 *
248 * @return Activity The Activity.
249 */
250 public function to_activity( $type ) {
251 $object = $this->to_object();
252
253 $activity = new Activity();
254 $activity->set_type( $type );
255
256 // Pre-fill the Activity with data (for example, cc and to).
257 $activity->set_object( $object );
258
259 // Use simple Object (only ID-URI) for Like and Announce.
260 if ( 'Like' === $type ) {
261 $activity->set_object( $object->get_id() );
262 }
263
264 return $activity;
265 }
266
267 /**
268 * Returns a generic locale based on the Blog settings.
269 *
270 * @return string The locale of the blog.
271 */
272 protected function get_locale() {
273 $lang = \strtolower( \strtok( \get_locale(), '_-' ) );
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
286 /**
287 * Returns the default media type for an Object.
288 *
289 * @return string The media type.
290 */
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 }
637 }
638