-
Notifications
You must be signed in to change notification settings - Fork 68
/
class-post.php
808 lines (697 loc) · 21.8 KB
/
class-post.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
<?php
namespace Activitypub\Transformer;
use WP_Post;
use Activitypub\Shortcodes;
use Activitypub\Model\Blog_User;
use Activitypub\Transformer\Base;
use Activitypub\Collection\Users;
use Activitypub\Activity\Base_Object;
use function Activitypub\esc_hashtag;
use function Activitypub\is_single_user;
use function Activitypub\get_rest_url_by_path;
use function Activitypub\site_supports_blocks;
/**
* WordPress Post Transformer
*
* The Post Transformer is responsible for transforming a WP_Post object into different other
* Object-Types.
*
* Currently supported are:
*
* - Activitypub\Activity\Base_Object
*/
class Post extends Base {
/**
* Returns the ID of the WordPress Post.
*
* @return int The ID of the WordPress Post
*/
public function get_wp_user_id() {
return $this->wp_object->post_author;
}
/**
* Change the User-ID of the WordPress Post.
*
* @return int The User-ID of the WordPress Post
*/
public function change_wp_user_id( $user_id ) {
$this->wp_object->post_author = $user_id;
return $this;
}
/**
* Transforms the WP_Post object to an ActivityPub Object
*
* @see \Activitypub\Activity\Base_Object
*
* @return \Activitypub\Activity\Base_Object The ActivityPub Object
*/
public function to_object() {
$post = $this->wp_object;
$object = parent::to_object();
$published = \strtotime( $post->post_date_gmt );
$object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) );
$updated = \strtotime( $post->post_modified_gmt );
if ( $updated > $published ) {
$object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) );
}
$object->set_content_map(
array(
$this->get_locale() => $this->get_content(),
)
);
$path = sprintf( 'users/%d/followers', intval( $post->post_author ) );
$object->set_to(
array(
'https://www.w3.org/ns/activitystreams#Public',
get_rest_url_by_path( $path ),
)
);
return $object;
}
/**
* Returns the ID of the Post.
*
* @return string The Posts ID.
*/
public function get_id() {
return $this->get_url();
}
/**
* Returns the URL of the Post.
*
* @return string The Posts URL.
*/
public function get_url() {
$post = $this->wp_object;
if ( 'trash' === get_post_status( $post ) ) {
$permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true );
} else {
$permalink = \get_permalink( $post );
}
return \esc_url( $permalink );
}
/**
* Returns the User-URL of the Author of the Post.
*
* If `single_user` mode is enabled, the URL of the Blog-User is returned.
*
* @return string The User-URL.
*/
protected function get_attributed_to() {
$blog_user = new Blog_User();
if ( is_single_user() ) {
return $blog_user->get_url();
}
$user = Users::get_by_id( $this->wp_object->post_author );
if ( $user && ! is_wp_error( $user ) ) {
return $user->get_url();
}
return $blog_user->get_url();
}
/**
* Generates all Media Attachments for a Post.
*
* @return array The Attachments.
*/
protected function get_attachment() {
// Once upon a time we only supported images, but we now support audio/video as well.
// We maintain the image-centric naming for backwards compatibility.
$max_media = \intval( \apply_filters( 'activitypub_max_image_attachments', \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ) );
if ( site_supports_blocks() && \has_blocks( $this->wp_object->post_content ) ) {
$media = $this->get_block_attachments( $max_media );
} else {
$media = $this->get_classic_editor_images( $max_media );
}
$unique_ids = \array_unique( \array_column( $media, 'id' ) );
$media = \array_intersect_key( $media, $unique_ids );
$media = \array_slice( $media, 0, $max_media );
return \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $media ) );
}
/**
* Get media attachments from blocks. They will be formatted as ActivityPub attachments, not as WP attachments.
*
* @param int $max_media The maximum number of attachments to return.
*
* @return array The attachments.
*/
protected function get_block_attachments( $max_media ) {
// max media can't be negative or zero
if ( $max_media <= 0 ) {
return array();
}
$id = $this->wp_object->ID;
$media = array(
'image' => array(),
'audio' => array(),
'video' => array(),
);
// list post thumbnail first if this post has one
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
$media['image'][] = array( 'id' => \get_post_thumbnail_id( $id ) );
}
if ( $max_media > 0 ) {
$blocks = \parse_blocks( $this->wp_object->post_content );
$media = self::get_media_from_blocks( $blocks, $media );
}
return self::filter_media_by_object_type( $media, \get_post_format( $this->wp_object ) );
}
/**
* Get image attachments from the classic editor.
* This is imperfect as the contained images aren't necessarily the
* same as the attachments.
*
* @param int $max_images The maximum number of images to return.
*
* @return array The attachment IDs.
*/
protected function get_classic_editor_image_attachments( $max_images ) {
// max images can't be negative or zero
if ( $max_images <= 0 ) {
return array();
}
$images = array();
$query = new \WP_Query(
array(
'post_parent' => $this->wp_object->ID,
'post_status' => 'inherit',
'post_type' => 'attachment',
'post_mime_type' => 'image',
'order' => 'ASC',
'orderby' => 'menu_order ID',
'posts_per_page' => $max_images,
)
);
foreach ( $query->get_posts() as $attachment ) {
if ( ! \in_array( $attachment->ID, $images, true ) ) {
$images[] = array( 'id' => $attachment->ID );
}
}
return $images;
}
/**
* Get image embeds from the classic editor by parsing HTML.
*
* @param int $max_images The maximum number of images to return.
*
* @return array The attachments.
*/
protected function get_classic_editor_image_embeds( $max_images ) {
// if someone calls that function directly, bail
if ( ! \class_exists( '\WP_HTML_Tag_Processor' ) ) {
return array();
}
// max images can't be negative or zero
if ( $max_images <= 0 ) {
return array();
}
$images = array();
$base = \wp_get_upload_dir()['baseurl'];
$content = \get_post_field( 'post_content', $this->wp_object );
$tags = new \WP_HTML_Tag_Processor( $content );
// This linter warning is a false positive - we have to
// re-count each time here as we modify $images.
// phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
while ( $tags->next_tag( 'img' ) && ( \count( $images ) <= $max_images ) ) {
$src = $tags->get_attribute( 'src' );
// If the img source is in our uploads dir, get the
// associated ID. Note: if there's a -500x500
// type suffix, we remove it, but we try the original
// first in case the original image is actually called
// that. Likewise, we try adding the -scaled suffix for
// the case that this is a small version of an image
// that was big enough to get scaled down on upload:
// https://make.wordpress.org/core/2019/10/09/introducing-handling-of-big-images-in-wordpress-5-3/
if ( null !== $src && \str_starts_with( $src, $base ) ) {
$img_id = \attachment_url_to_postid( $src );
if ( 0 === $img_id ) {
$count = 0;
$src = preg_replace( '/-(?:\d+x\d+)(\.[a-zA-Z]+)$/', '$1', $src, 1, $count );
if ( $count > 0 ) {
$img_id = \attachment_url_to_postid( $src );
}
}
if ( 0 === $img_id ) {
$src = preg_replace( '/(\.[a-zA-Z]+)$/', '-scaled$1', $src );
$img_id = \attachment_url_to_postid( $src );
}
if ( 0 !== $img_id ) {
$images[] = array(
'id' => $img_id,
'alt' => $tags->get_attribute( 'alt' ),
);
}
}
}
return $images;
}
/**
* Get post images from the classic editor.
* Note that audio/video attachments are only supported in the block editor.
*
* @param int $max_images The maximum number of images to return.
*
* @return array The attachments.
*/
protected function get_classic_editor_images( $max_images ) {
// max images can't be negative or zero
if ( $max_images <= 0 ) {
return array();
}
$id = $this->wp_object->ID;
$images = array();
// list post thumbnail first if this post has one
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
$images[] = \get_post_thumbnail_id( $id );
}
if ( \count( $images ) <= $max_images ) {
if ( \class_exists( '\WP_HTML_Tag_Processor' ) ) {
$images = \array_merge( $images, $this->get_classic_editor_image_embeds( $max_images ) );
} else {
$images = \array_merge( $images, $this->get_classic_editor_image_attachments( $max_images ) );
}
}
return $images;
}
/**
* Recursively get media IDs from blocks.
* @param array $blocks The blocks to search for media IDs
* @param array $media The media IDs to append new IDs to
* @param int $max_media The maximum number of media to return.
*
* @return array The image IDs.
*/
protected static function get_media_from_blocks( $blocks, $media ) {
foreach ( $blocks as $block ) {
// recurse into inner blocks
if ( ! empty( $block['innerBlocks'] ) ) {
$media = self::get_media_from_blocks( $block['innerBlocks'], $media );
}
switch ( $block['blockName'] ) {
case 'core/image':
case 'core/cover':
if ( ! empty( $block['attrs']['id'] ) ) {
$alt = '';
$check = preg_match( '/<img.*?alt\s*=\s*([\"\'])(.*?)\1.*>/i', $block['innerHTML'], $match );
if ( $check ) {
$alt = $match[2];
}
$media['image'][] = array(
'id' => $block['attrs']['id'],
'alt' => $alt,
);
}
break;
case 'core/audio':
if ( ! empty( $block['attrs']['id'] ) ) {
$media['audio'][] = array( 'id' => $block['attrs']['id'] );
}
break;
case 'core/video':
case 'videopress/video':
if ( ! empty( $block['attrs']['id'] ) ) {
$media['video'][] = array( 'id' => $block['attrs']['id'] );
}
break;
case 'jetpack/slideshow':
case 'jetpack/tiled-gallery':
if ( ! empty( $block['attrs']['ids'] ) ) {
$media['image'] = array_merge(
$media['image'],
array_map(
function ( $id ) {
return array( 'id' => $id );
},
$block['attrs']['ids']
)
);
}
break;
case 'jetpack/image-compare':
if ( ! empty( $block['attrs']['beforeImageId'] ) ) {
$media['image'][] = array( 'id' => $block['attrs']['beforeImageId'] );
}
if ( ! empty( $block['attrs']['afterImageId'] ) ) {
$media['image'][] = array( 'id' => $block['attrs']['afterImageId'] );
}
break;
}
}
return $media;
}
/**
* Filter media IDs by object type.
*
* @param array $media The media array grouped by type.
* @param array $type The object type.
*
* @return array The filtered media IDs.
*/
protected static function filter_media_by_object_type( $media, $type ) {
$type = \apply_filters( 'filter_media_by_object_type', \strtolower( $type ) );
if ( ! empty( $media[ $type ] ) ) {
return $media[ $type ];
}
return array_merge( array(), ...array_values( $media ) );
}
/**
* Converts a WordPress Attachment to an ActivityPub Attachment.
*
* @param array $media The Attachment array.
*
* @return array The ActivityPub Attachment.
*/
public static function wp_attachment_to_activity_attachment( $media ) {
$id = $media['id'];
$attachment = array();
$mime_type = \get_post_mime_type( $id );
$mime_type_parts = \explode( '/', $mime_type );
// switching on image/audio/video
switch ( $mime_type_parts[0] ) {
case 'image':
$image_size = 'large';
/**
* Filter the image URL returned for each post.
*
* @param array|false $thumbnail The image URL, or false if no image is available.
* @param int $id The attachment ID.
* @param string $image_size The image size to retrieve. Set to 'large' by default.
*/
$thumbnail = apply_filters(
'activitypub_get_image',
self::get_wordpress_attachment( $id, $image_size ),
$id,
$image_size
);
if ( $thumbnail ) {
$image = array(
'type' => 'Image',
'url' => \esc_url( $thumbnail[0] ),
'mediaType' => \esc_attr( $mime_type ),
);
if ( ! empty( $media['alt'] ) ) {
$image['name'] = \wp_strip_all_tags( \html_entity_decode( $media['alt'] ) );
} else {
$alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
if ( $alt ) {
$image['name'] = \wp_strip_all_tags( \html_entity_decode( $alt ) );
}
}
$attachment = $image;
}
break;
case 'audio':
case 'video':
$attachment = array(
'type' => 'Document',
'mediaType' => \esc_attr( $mime_type ),
'url' => \esc_url( \wp_get_attachment_url( $id ) ),
'name' => \esc_attr( \get_the_title( $id ) ),
);
$meta = wp_get_attachment_metadata( $id );
// height and width for videos
if ( isset( $meta['width'] ) && isset( $meta['height'] ) ) {
$attachment['width'] = \esc_attr( $meta['width'] );
$attachment['height'] = \esc_attr( $meta['height'] );
}
// @todo: add `icon` support for audio/video attachments. Maybe use post thumbnail?
break;
}
return \apply_filters( 'activitypub_attachment', $attachment, $id );
}
/**
* Return details about an image attachment.
*
* @param int $id The attachment ID.
* @param string $image_size The image size to retrieve. Set to 'large' by default.
*
* @return array|false Array of image data, or boolean false if no image is available.
*/
protected static function get_wordpress_attachment( $id, $image_size = 'large' ) {
/**
* Hook into the image retrieval process. Before image retrieval.
*
* @param int $id The attachment ID.
* @param string $image_size The image size to retrieve. Set to 'large' by default.
*/
do_action( 'activitypub_get_image_pre', $id, $image_size );
$image = \wp_get_attachment_image_src( $id, $image_size );
/**
* Hook into the image retrieval process. After image retrieval.
*
* @param int $id The attachment ID.
* @param string $image_size The image size to retrieve. Set to 'large' by default.
*/
do_action( 'activitypub_get_image_post', $id, $image_size );
return $image;
}
/**
* Returns the ActivityStreams 2.0 Object-Type for a Post based on the
* settings and the Post-Type.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
*
* @return string The Object-Type.
*/
protected function get_type() {
$post_format_setting = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
if ( 'wordpress-post-format' !== $post_format_setting ) {
return \ucfirst( $post_format_setting );
}
$has_title = post_type_supports( $this->wp_object->post_type, 'title' );
if ( ! $has_title ) {
return 'Note';
}
// Default to Article.
$object_type = 'Note';
$post_format = 'standard';
if ( \get_theme_support( 'post-formats' ) ) {
$post_format = \get_post_format( $this->wp_object );
}
$post_type = \get_post_type( $this->wp_object );
switch ( $post_type ) {
case 'post':
switch ( $post_format ) {
case 'standard':
case '':
$object_type = 'Article';
break;
default:
$object_type = 'Note';
break;
}
break;
case 'page':
$object_type = 'Page';
break;
default:
$object_type = 'Note';
break;
}
return $object_type;
}
/**
* Returns a list of Mentions, used in the Post.
*
* @see https://docs.joinmastodon.org/spec/activitypub/#Mention
*
* @return array The list of Mentions.
*/
protected function get_cc() {
$cc = array();
$mentions = $this->get_mentions();
if ( $mentions ) {
foreach ( $mentions as $url ) {
$cc[] = $url;
}
}
return $cc;
}
/**
* Returns a list of Tags, used in the Post.
*
* This includes Hash-Tags and Mentions.
*
* @return array The list of Tags.
*/
protected function get_tag() {
$tags = array();
$post_tags = \get_the_tags( $this->wp_object->ID );
if ( $post_tags ) {
foreach ( $post_tags as $post_tag ) {
$tag = array(
'type' => 'Hashtag',
'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ),
'name' => esc_hashtag( $post_tag->name ),
);
$tags[] = $tag;
}
}
$mentions = $this->get_mentions();
if ( $mentions ) {
foreach ( $mentions as $mention => $url ) {
$tag = array(
'type' => 'Mention',
'href' => \esc_url( $url ),
'name' => \esc_html( $mention ),
);
$tags[] = $tag;
}
}
return $tags;
}
/**
* Returns the summary for the ActivityPub Item.
*
* The summary will be generated based on the user settings and only if the
* object type is not set to `note`.
*
* @return string|null The summary or null if the object type is `note`.
*/
protected function get_summary() {
if ( 'Note' === $this->get_type() ) {
return null;
}
$content = \get_post_field( 'post_content', $this->wp_object->ID );
$content = \html_entity_decode( $content );
$content = \wp_strip_all_tags( $content );
$content = \trim( $content );
$content = \preg_replace( '/\R+/m', "\n\n", $content );
$content = \preg_replace( '/[\r\t]/', '', $content );
$excerpt_more = \apply_filters( 'activitypub_excerpt_more', '[...]' );
$length = 500;
$length = $length - strlen( $excerpt_more );
if ( \strlen( $content ) > $length ) {
$content = \wordwrap( $content, $length, '</activitypub-summary>' );
$content = \explode( '</activitypub-summary>', $content, 2 );
$content = $content[0];
}
return $content . ' ' . $excerpt_more;
}
/**
* Returns the title for the ActivityPub Item.
*
* The title will be generated based on the user settings and only if the
* object type is not set to `note`.
*
* @return string|null The title or null if the object type is `note`.
*/
protected function get_name() {
if ( 'Note' === $this->get_type() ) {
return null;
}
$title = \get_the_title( $this->wp_object->ID );
if ( $title ) {
return \wp_strip_all_tags(
\html_entity_decode(
$title
)
);
}
return null;
}
/**
* Returns the content for the ActivityPub Item.
*
* The content will be generated based on the user settings.
*
* @return string The content.
*/
protected function get_content() {
global $post;
/**
* Provides an action hook so plugins can add their own hooks/filters before AP content is generated.
*
* 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.
*
* @param WP_Post $post The post object.
*/
do_action( 'activitypub_before_get_content', $post );
add_filter( 'render_block_core/embed', array( self::class, 'revert_embed_links' ), 10, 2 );
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$post = $this->wp_object;
$content = $this->get_post_content_template();
// Register our shortcodes just in time.
Shortcodes::register();
// Fill in the shortcodes.
setup_postdata( $post );
$content = do_shortcode( $content );
wp_reset_postdata();
$content = \wpautop( $content );
$content = \preg_replace( '/[\n\r\t]/', '', $content );
$content = \trim( $content );
$content = \apply_filters( 'activitypub_the_content', $content, $post );
// Don't need these any more, should never appear in a post.
Shortcodes::unregister();
return $content;
}
/**
* Gets the template to use to generate the content of the activitypub item.
*
* @return string The Template.
*/
protected function get_post_content_template() {
$type = \get_option( 'activitypub_post_content_type', 'content' );
switch ( $type ) {
case 'excerpt':
$template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
break;
case 'title':
$template = "<h2>[ap_title]</h2>\n\n[ap_permalink type=\"html\"]";
break;
case 'content':
$template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
break;
default:
$template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
break;
}
$post_format_setting = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
if ( 'wordpress-post-format' === $post_format_setting ) {
$template = '[ap_content]';
}
return apply_filters( 'activitypub_object_content_template', $template, $this->wp_object );
}
/**
* Helper function to get the @-Mentions from the post content.
*
* @return array The list of @-Mentions.
*/
protected function get_mentions() {
return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_object->post_content, $this->wp_object );
}
/**
* Returns the locale of the post.
*
* @return string The locale of the post.
*/
public function get_locale() {
$post_id = $this->wp_object->ID;
$lang = \strtolower( \strtok( \get_locale(), '_-' ) );
/**
* Filter the locale of the post.
*
* @param string $lang The locale of the post.
* @param int $post_id The post ID.
* @param WP_Post $post The post object.
*
* @return string The filtered locale of the post.
*/
return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->wp_object );
}
/**
* Transform Embed blocks to block level link.
*
* Remote servers will simply drop iframe elements, rendering incomplete content.
*
* @see https://www.w3.org/TR/activitypub/#security-sanitizing-content
* @see https://www.w3.org/wiki/ActivityPub/Primer/HTML
*
* @param string $block_content The block content (html)
* @param object $block The block object
*
* @return string A block level link
*/
public static function revert_embed_links( $block_content, $block ) {
return '<p><a href="' . esc_url( $block['attrs']['url'] ) . '">' . $block['attrs']['url'] . '</a></p>';
}
}