PluginProbe
ActivityPub / 5.7.0
ActivityPub v5.7.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
← All changes | includes/class-blocks.php +297 -33 1.2.05.7.0 View file →
@@ -1,35 +1,135 @@
1 1 <?php
2 +/**
3 + * Blocks file.
4 + *
5 + * @package Activitypub
6 + */
7 +
2 8 namespace Activitypub;
3 9
10 +use Activitypub\Collection\Actors;
4 11 use Activitypub\Collection\Followers;
5 -use Activitypub\Collection\Users as User_Collection;
6 -use Activitypub\is_user_type_disabled;
7 12
13 +/**
14 + * Block class.
15 + */
8 16 class Blocks {
17 + /**
18 + * Initialize the class, registering WordPress hooks.
19 + */
9 20 public static function init() {
10 - // this is already being called on the init hook, so just add it.
21 + // This is already being called on the init hook, so just add it.
11 22 self::register_blocks();
12 - \add_action( 'wp_enqueue_scripts', array( self::class, 'add_data' ) );
13 - \add_action( 'enqueue_block_editor_assets', array( self::class, 'add_data' ) );
23 +
24 + \add_action( 'wp_head', array( self::class, 'inject_activitypub_options' ), 11 );
25 + \add_action( 'admin_print_scripts', array( self::class, 'inject_activitypub_options' ) );
26 + \add_action( 'load-post-new.php', array( self::class, 'handle_in_reply_to_get_param' ) );
27 + // Add editor plugin.
28 + \add_action( 'enqueue_block_editor_assets', array( self::class, 'enqueue_editor_assets' ) );
29 + \add_action( 'init', array( self::class, 'register_postmeta' ), 11 );
14 30 }
15 31
16 - public static function add_data() {
17 - $context = is_admin() ? 'editor' : 'view';
18 - $followers_handle = 'activitypub-followers-' . $context . '-script';
19 - $follow_me_handle = 'activitypub-follow-me-' . $context . '-script';
32 + /**
33 + * Register post meta for content warnings.
34 + */
35 + public static function register_postmeta() {
36 + $ap_post_types = \get_post_types_by_support( 'activitypub' );
37 + foreach ( $ap_post_types as $post_type ) {
38 + \register_post_meta(
39 + $post_type,
40 + 'activitypub_content_warning',
41 + array(
42 + 'show_in_rest' => true,
43 + 'single' => true,
44 + 'type' => 'string',
45 + 'sanitize_callback' => function ( $warning ) {
46 + if ( $warning ) {
47 + return \sanitize_text_field( $warning );
48 + }
49 +
50 + return null;
51 + },
52 + )
53 + );
54 +
55 + \register_post_meta(
56 + $post_type,
57 + 'activitypub_content_visibility',
58 + array(
59 + 'type' => 'string',
60 + 'single' => true,
61 + 'show_in_rest' => true,
62 + 'sanitize_callback' => function ( $value ) {
63 + $schema = array(
64 + 'type' => 'string',
65 + 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ),
66 + 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC,
67 + );
68 +
69 + if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
70 + return $schema['default'];
71 + }
72 +
73 + return $value;
74 + },
75 + )
76 + );
77 + }
78 + }
79 +
80 + /**
81 + * Enqueue the block editor assets.
82 + */
83 + public static function enqueue_editor_assets() {
84 + // Check for our supported post types.
85 + $current_screen = \get_current_screen();
86 + $ap_post_types = \get_post_types_by_support( 'activitypub' );
87 + if ( ! $current_screen || ! in_array( $current_screen->post_type, $ap_post_types, true ) ) {
88 + return;
89 + }
90 + $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/editor-plugin/plugin.asset.php';
91 + $plugin_url = plugins_url( 'build/editor-plugin/plugin.js', ACTIVITYPUB_PLUGIN_FILE );
92 + wp_enqueue_script( 'activitypub-block-editor', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true );
93 + }
94 +
95 + /**
96 + * Enqueue the reply handle script if the in_reply_to GET param is set.
97 + */
98 + public static function handle_in_reply_to_get_param() {
99 + // Only load the script if the in_reply_to GET param is set, action happens there, not here.
100 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
101 + if ( ! isset( $_GET['in_reply_to'] ) ) {
102 + return;
103 + }
104 +
105 + $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/reply-intent/plugin.asset.php';
106 + $plugin_url = plugins_url( 'build/reply-intent/plugin.js', ACTIVITYPUB_PLUGIN_FILE );
107 + wp_enqueue_script( 'activitypub-reply-intent', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true );
108 + }
109 +
110 + /**
111 + * Output ActivityPub options as a script tag.
112 + */
113 + public static function inject_activitypub_options() {
20 114 $data = array(
21 - 'namespace' => ACTIVITYPUB_REST_NAMESPACE,
22 - 'enabled' => array(
23 - 'site' => ! is_user_type_disabled( 'blog' ),
115 + 'namespace' => ACTIVITYPUB_REST_NAMESPACE,
116 + 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg',
117 + 'enabled' => array(
118 + 'site' => ! is_user_type_disabled( 'blog' ),
24 119 'users' => ! is_user_type_disabled( 'user' ),
25 120 ),
26 121 );
27 - $js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) );
28 - \wp_add_inline_script( $followers_handle, $js, 'before' );
29 - \wp_add_inline_script( $follow_me_handle, $js, 'before' );
122 +
123 + printf(
124 + "\n<script>var _activityPubOptions = %s;</script>",
125 + wp_json_encode( $data )
126 + );
30 127 }
31 128
129 + /**
130 + * Register the blocks.
131 + */
32 132 public static function register_blocks() {
33 133 \register_block_type_from_metadata(
34 134 ACTIVITYPUB_PLUGIN_DIR . '/build/followers',
35 135 array(
@@ -41,45 +141,137 @@
41 141 array(
42 142 'render_callback' => array( self::class, 'render_follow_me_block' ),
43 143 )
44 144 );
145 + \register_block_type_from_metadata(
146 + ACTIVITYPUB_PLUGIN_DIR . '/build/reply',
147 + array(
148 + 'render_callback' => array( self::class, 'render_reply_block' ),
149 + )
150 + );
151 +
152 + \register_block_type_from_metadata(
153 + ACTIVITYPUB_PLUGIN_DIR . '/build/reactions',
154 + array(
155 + 'render_callback' => array( self::class, 'render_post_reactions_block' ),
156 + )
157 + );
45 158 }
46 159
160 + /**
161 + * Render the post reactions block.
162 + *
163 + * @param array $attrs The block attributes.
164 + *
165 + * @return string The HTML to render.
166 + */
167 + public static function render_post_reactions_block( $attrs ) {
168 + if ( ! isset( $attrs['postId'] ) ) {
169 + $attrs['postId'] = get_the_ID();
170 + }
171 +
172 + $wrapper_attributes = get_block_wrapper_attributes(
173 + array(
174 + 'class' => 'activitypub-reactions-block',
175 + 'data-attrs' => wp_json_encode( $attrs ),
176 + )
177 + );
178 +
179 + return sprintf(
180 + '<div %s></div>',
181 + $wrapper_attributes
182 + );
183 + }
184 +
185 + /**
186 + * Get the user ID from a user string.
187 + *
188 + * @param string $user_string The user string. Can be a user ID, 'site', or 'inherit'.
189 + * @return int|null The user ID, or null if the 'inherit' string is not supported in this context.
190 + */
47 191 private static function get_user_id( $user_string ) {
48 192 if ( is_numeric( $user_string ) ) {
49 193 return absint( $user_string );
50 194 }
51 - // any other non-numeric falls back to 0, including the `site` string used in the UI
52 - return 0;
195 +
196 + // If the user string is 'site', return the Blog User ID.
197 + if ( 'site' === $user_string ) {
198 + return Actors::BLOG_USER_ID;
199 + }
200 +
201 + // The only other value should be 'inherit', which means to use the query context to determine the User.
202 + if ( 'inherit' !== $user_string ) {
203 + return null;
204 + }
205 +
206 + // For a homepage/front page, if the Blog User is active, use it.
207 + if ( ( is_front_page() || is_home() ) && ! is_user_type_disabled( 'blog' ) ) {
208 + return Actors::BLOG_USER_ID;
209 + }
210 +
211 + // If we're in a loop, use the post author.
212 + $author_id = get_the_author_meta( 'ID' );
213 + if ( $author_id ) {
214 + return $author_id;
215 + }
216 +
217 + // For other pages, the queried object will clue us in.
218 + $queried_object = get_queried_object();
219 + if ( ! $queried_object ) {
220 + return null;
221 + }
222 +
223 + // If we're on a user archive page, use that user's ID.
224 + if ( is_a( $queried_object, 'WP_User' ) ) {
225 + return $queried_object->ID;
226 + }
227 +
228 + // For a single post, use the post author's ID.
229 + if ( is_a( $queried_object, 'WP_Post' ) ) {
230 + return get_the_author_meta( 'ID' );
231 + }
232 +
233 + // We won't properly account for some conditions, like tag archives.
234 + return null;
53 235 }
54 236
55 237 /**
56 238 * Filter an array by a list of keys.
57 - * @param array $array The array to filter.
239 + *
240 + * @param array $data The array to filter.
58 241 * @param array $keys The keys to keep.
59 242 * @return array The filtered array.
60 243 */
61 - protected static function filter_array_by_keys( $array, $keys ) {
62 - return array_intersect_key( $array, array_flip( $keys ) );
244 + protected static function filter_array_by_keys( $data, $keys ) {
245 + return array_intersect_key( $data, array_flip( $keys ) );
63 246 }
64 247
65 248 /**
66 249 * Render the follow me block.
250 + *
67 251 * @param array $attrs The block attributes.
68 252 * @return string The HTML to render.
69 253 */
70 254 public static function render_follow_me_block( $attrs ) {
71 255 $user_id = self::get_user_id( $attrs['selectedUser'] );
72 - $user = User_Collection::get_by_id( $user_id );
73 - if ( ! is_wp_error( $user ) ) {
74 - $attrs['profileData'] = self::filter_array_by_keys(
75 - $user->to_array(),
76 - array( 'icon', 'name', 'resource' )
77 - );
256 + $user = Actors::get_by_id( $user_id );
257 + if ( is_wp_error( $user ) ) {
258 + if ( 'inherit' === $attrs['selectedUser'] ) {
259 + // If the user is 'inherit' and we couldn't determine the user, don't render anything.
260 + return '<!-- Follow Me block: `inherit` mode does not display on this type of page -->';
261 + } else {
262 + // If the user is a specific ID and we couldn't find it, render an error message.
263 + return '<!-- Follow Me block: user not found -->';
264 + }
78 265 }
266 +
267 + $attrs['profileData'] = self::filter_array_by_keys(
268 + $user->to_array(),
269 + array( 'icon', 'name', 'webfinger' )
270 + );
271 +
79 272 $wrapper_attributes = get_block_wrapper_attributes(
80 273 array(
81 - 'aria-label' => __( 'Follow me on the Fediverse', 'activitypub' ),
82 274 'class' => 'activitypub-follow-me-block-wrapper',
83 275 'data-attrs' => wp_json_encode( $attrs ),
84 276 )
85 277 );
@@ -86,16 +278,32 @@
86 278 // todo: render more than an empty div?
87 279 return '<div ' . $wrapper_attributes . '></div>';
88 280 }
89 281
282 + /**
283 + * Render the follower block.
284 + *
285 + * @param array $attrs The block attributes.
286 + *
287 + * @return string The HTML to render.
288 + */
90 289 public static function render_follower_block( $attrs ) {
91 290 $followee_user_id = self::get_user_id( $attrs['selectedUser'] );
92 - $per_page = absint( $attrs['per_page'] );
291 + if ( is_null( $followee_user_id ) ) {
292 + return '<!-- Followers block: `inherit` mode does not display on this type of page -->';
293 + }
294 +
295 + $user = Actors::get_by_id( $followee_user_id );
296 + if ( is_wp_error( $user ) ) {
297 + return '<!-- Followers block: `' . $followee_user_id . '` not an active ActivityPub user -->';
298 + }
299 +
300 + $per_page = absint( $attrs['per_page'] );
93 301 $follower_data = Followers::get_followers_with_count( $followee_user_id, $per_page );
94 302
95 - $attrs['followerData']['total'] = $follower_data['total'];
303 + $attrs['followerData']['total'] = $follower_data['total'];
96 304 $attrs['followerData']['followers'] = array_map(
97 - function( $follower ) {
305 + function ( $follower ) {
98 306 return self::filter_array_by_keys(
99 307 $follower->to_array(),
100 308 array( 'icon', 'name', 'preferredUsername', 'url' )
101 309 );
@@ -101,9 +309,9 @@
101 309 );
102 310 },
103 311 $follower_data['followers']
104 312 );
105 - $wrapper_attributes = get_block_wrapper_attributes(
313 + $wrapper_attributes = get_block_wrapper_attributes(
106 314 array(
107 315 'aria-label' => __( 'Fediverse Followers', 'activitypub' ),
108 316 'class' => 'activitypub-follower-block',
109 317 'data-attrs' => wp_json_encode( $attrs ),
@@ -122,11 +330,67 @@
122 330 $html .= '</ul></div>';
123 331 return $html;
124 332 }
125 333
334 + /**
335 + * Render the reply block.
336 + *
337 + * @param array $attrs The block attributes.
338 + *
339 + * @return string The HTML to render.
340 + */
341 + public static function render_reply_block( $attrs ) {
342 + // Return early if no URL is provided.
343 + if ( empty( $attrs['url'] ) ) {
344 + return null;
345 + }
346 +
347 + $show_embed = isset( $attrs['embedPost'] ) && $attrs['embedPost'];
348 +
349 + $wrapper_attrs = get_block_wrapper_attributes(
350 + array(
351 + 'aria-label' => __( 'Reply', 'activitypub' ),
352 + 'class' => 'activitypub-reply-block',
353 + 'data-in-reply-to' => $attrs['url'],
354 + )
355 + );
356 +
357 + $html = '<div ' . $wrapper_attrs . '>';
358 +
359 + // Try to get and append the embed if requested.
360 + if ( $show_embed ) {
361 + $embed = wp_oembed_get( $attrs['url'] );
362 + if ( $embed ) {
363 + $html .= $embed;
364 + }
365 + }
366 +
367 + // Only show the link if we're not showing the embed.
368 + if ( ! $show_embed ) {
369 + $html .= sprintf(
370 + '<p><a title="%2$s" aria-label="%2$s" href="%1$s" class="u-in-reply-to" target="_blank">%3$s</a></p>',
371 + esc_url( $attrs['url'] ),
372 + esc_attr__( 'This post is a response to the referenced content.', 'activitypub' ),
373 + // translators: %s is the URL of the post being replied to.
374 + sprintf( __( '&#8620;%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', esc_url( $attrs['url'] ) ) )
375 + );
376 + }
377 +
378 + $html .= '</div>';
379 +
380 + return $html;
381 + }
382 +
383 + /**
384 + * Render a follower.
385 + *
386 + * @param \Activitypub\Model\Follower $follower The follower to render.
387 + *
388 + * @return string The HTML to render.
389 + */
126 390 public static function render_follower( $follower ) {
127 391 $external_svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="components-external-link__icon css-rvs7bx esh4a730" aria-hidden="true" focusable="false"><path d="M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"></path></svg>';
128 - $template =
392 + $template =
129 393 '<a href="%s" title="%s" class="components-external-link activitypub-link" target="_blank" rel="external noreferrer noopener">
130 394 <img width="40" height="40" src="%s" class="avatar activitypub-avatar" />
131 395 <span class="activitypub-actor">
132 396 <strong class="activitypub-name">%s</strong>
@@ -139,9 +403,9 @@
139 403 $data = $follower->to_array();
140 404
141 405 return sprintf(
142 406 $template,
143 - esc_url( $data['url'] ),
407 + esc_url( object_to_uri( $data['url'] ) ),
144 408 esc_attr( $data['name'] ),
145 409 esc_attr( $data['icon']['url'] ),
146 410 esc_html( $data['name'] ),
147 411 esc_html( $data['preferredUsername'] ),