PluginProbe
ActivityPub / 5.2.0
ActivityPub v5.2.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 / class-blocks.php

class-blocks.php in ActivityPub 5.2.0, at includes/class-blocks.php

396 lines 11.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Blocks file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Activitypub\Collection\Followers;
11 use Activitypub\Collection\Actors;
12
13 /**
14 * Block class.
15 */
16 class Blocks {
17 /**
18 * Initialize the class, registering WordPress hooks.
19 */
20 public static function init() {
21 // This is already being called on the init hook, so just add it.
22 self::register_blocks();
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 );
30 }
31
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() {
114 $data = array(
115 'namespace' => ACTIVITYPUB_REST_NAMESPACE,
116 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg',
117 'enabled' => array(
118 'site' => ! is_user_type_disabled( 'blog' ),
119 'users' => ! is_user_type_disabled( 'user' ),
120 ),
121 );
122
123 printf(
124 "\n<script>var _activityPubOptions = %s;</script>",
125 wp_json_encode( $data )
126 );
127 }
128
129 /**
130 * Register the blocks.
131 */
132 public static function register_blocks() {
133 \register_block_type_from_metadata(
134 ACTIVITYPUB_PLUGIN_DIR . '/build/followers',
135 array(
136 'render_callback' => array( self::class, 'render_follower_block' ),
137 )
138 );
139 \register_block_type_from_metadata(
140 ACTIVITYPUB_PLUGIN_DIR . '/build/follow-me',
141 array(
142 'render_callback' => array( self::class, 'render_follow_me_block' ),
143 )
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 );
158 }
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 */
191 private static function get_user_id( $user_string ) {
192 if ( is_numeric( $user_string ) ) {
193 return absint( $user_string );
194 }
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;
235 }
236
237 /**
238 * Filter an array by a list of keys.
239 *
240 * @param array $data The array to filter.
241 * @param array $keys The keys to keep.
242 * @return array The filtered array.
243 */
244 protected static function filter_array_by_keys( $data, $keys ) {
245 return array_intersect_key( $data, array_flip( $keys ) );
246 }
247
248 /**
249 * Render the follow me block.
250 *
251 * @param array $attrs The block attributes.
252 * @return string The HTML to render.
253 */
254 public static function render_follow_me_block( $attrs ) {
255 $user_id = self::get_user_id( $attrs['selectedUser'] );
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 }
265 }
266
267 $attrs['profileData'] = self::filter_array_by_keys(
268 $user->to_array(),
269 array( 'icon', 'name', 'webfinger' )
270 );
271
272 $wrapper_attributes = get_block_wrapper_attributes(
273 array(
274 'class' => 'activitypub-follow-me-block-wrapper',
275 'data-attrs' => wp_json_encode( $attrs ),
276 )
277 );
278 // todo: render more than an empty div?
279 return '<div ' . $wrapper_attributes . '></div>';
280 }
281
282 /**
283 * Render the follower block.
284 *
285 * @param array $attrs The block attributes.
286 *
287 * @return string The HTML to render.
288 */
289 public static function render_follower_block( $attrs ) {
290 $followee_user_id = self::get_user_id( $attrs['selectedUser'] );
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'] );
301 $follower_data = Followers::get_followers_with_count( $followee_user_id, $per_page );
302
303 $attrs['followerData']['total'] = $follower_data['total'];
304 $attrs['followerData']['followers'] = array_map(
305 function ( $follower ) {
306 return self::filter_array_by_keys(
307 $follower->to_array(),
308 array( 'icon', 'name', 'preferredUsername', 'url' )
309 );
310 },
311 $follower_data['followers']
312 );
313 $wrapper_attributes = get_block_wrapper_attributes(
314 array(
315 'aria-label' => __( 'Fediverse Followers', 'activitypub' ),
316 'class' => 'activitypub-follower-block',
317 'data-attrs' => wp_json_encode( $attrs ),
318 )
319 );
320
321 $html = '<div ' . $wrapper_attributes . '>';
322 if ( $attrs['title'] ) {
323 $html .= '<h3>' . esc_html( $attrs['title'] ) . '</h3>';
324 }
325 $html .= '<ul>';
326 foreach ( $follower_data['followers'] as $follower ) {
327 $html .= '<li>' . self::render_follower( $follower ) . '</li>';
328 }
329 // We are only pagination on the JS side. Could be revisited but we gotta ship!
330 $html .= '</ul></div>';
331 return $html;
332 }
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 $html = '';
343
344 if ( ! empty( $attrs['url'] ) ) {
345 $html = sprintf(
346 '<p><a title="%2$s" aria-label="%2$s" href="%1$s" class="u-in-reply-to" target="_blank">%3$s</a></p>',
347 esc_url( $attrs['url'] ),
348 esc_attr__( 'This post is a response to the referenced content.', 'activitypub' ),
349 // translators: %s is the URL of the post being replied to.
350 sprintf( __( '&#8620;%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', esc_url( $attrs['url'] ) ) )
351 );
352 }
353
354 /**
355 * Filter the reply block.
356 *
357 * @param string $html The HTML to render.
358 * @param array $attrs The block attributes.
359 */
360 return apply_filters( 'activitypub_reply_block', $html, $attrs );
361 }
362
363 /**
364 * Render a follower.
365 *
366 * @param \Activitypub\Model\Follower $follower The follower to render.
367 *
368 * @return string The HTML to render.
369 */
370 public static function render_follower( $follower ) {
371 $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>';
372 $template =
373 '<a href="%s" title="%s" class="components-external-link activitypub-link" target="_blank" rel="external noreferrer noopener">
374 <img width="40" height="40" src="%s" class="avatar activitypub-avatar" />
375 <span class="activitypub-actor">
376 <strong class="activitypub-name">%s</strong>
377 <span class="sep">/</span>
378 <span class="activitypub-handle">@%s</span>
379 </span>
380 %s
381 </a>';
382
383 $data = $follower->to_array();
384
385 return sprintf(
386 $template,
387 esc_url( object_to_uri( $data['url'] ) ),
388 esc_attr( $data['name'] ),
389 esc_attr( $data['icon']['url'] ),
390 esc_html( $data['name'] ),
391 esc_html( $data['preferredUsername'] ),
392 $external_svg
393 );
394 }
395 }
396