PluginProbe
ActivityPub / 4.5.1
ActivityPub v4.5.1
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 4.5.1, at includes/class-blocks.php

393 lines 11.6 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_enqueue_scripts', array( self::class, 'inject_activitypub_options' ) );
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 \register_post_meta(
55 $post_type,
56 'activitypub_content_visibility',
57 array(
58 'show_in_rest' => true,
59 'single' => true,
60 'type' => 'string',
61 'sanitize_callback' => function ( $visibility ) {
62 $options = array(
63 ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC,
64 ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL,
65 );
66
67 if ( in_array( $visibility, $options, true ) ) {
68 return $visibility;
69 }
70
71 return null;
72 },
73 )
74 );
75 }
76 }
77
78 /**
79 * Enqueue the block editor assets.
80 */
81 public static function enqueue_editor_assets() {
82 // Check for our supported post types.
83 $current_screen = \get_current_screen();
84 $ap_post_types = \get_post_types_by_support( 'activitypub' );
85 if ( ! $current_screen || ! in_array( $current_screen->post_type, $ap_post_types, true ) ) {
86 return;
87 }
88 $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/editor-plugin/plugin.asset.php';
89 $plugin_url = plugins_url( 'build/editor-plugin/plugin.js', ACTIVITYPUB_PLUGIN_FILE );
90 wp_enqueue_script( 'activitypub-block-editor', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true );
91 }
92
93 /**
94 * Enqueue the reply handle script if the in_reply_to GET param is set.
95 */
96 public static function handle_in_reply_to_get_param() {
97 // Only load the script if the in_reply_to GET param is set, action happens there, not here.
98 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
99 if ( ! isset( $_GET['in_reply_to'] ) ) {
100 return;
101 }
102
103 $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/reply-intent/plugin.asset.php';
104 $plugin_url = plugins_url( 'build/reply-intent/plugin.js', ACTIVITYPUB_PLUGIN_FILE );
105 wp_enqueue_script( 'activitypub-reply-intent', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true );
106 }
107
108 /**
109 * Output ActivityPub options as a script tag.
110 */
111 public static function inject_activitypub_options() {
112 $data = array(
113 'namespace' => ACTIVITYPUB_REST_NAMESPACE,
114 'enabled' => array(
115 'site' => ! is_user_type_disabled( 'blog' ),
116 'users' => ! is_user_type_disabled( 'user' ),
117 ),
118 );
119
120 printf(
121 '<script>var _activityPubOptions = %s;</script>',
122 wp_json_encode( $data )
123 );
124 }
125
126 /**
127 * Register the blocks.
128 */
129 public static function register_blocks() {
130 \register_block_type_from_metadata(
131 ACTIVITYPUB_PLUGIN_DIR . '/build/followers',
132 array(
133 'render_callback' => array( self::class, 'render_follower_block' ),
134 )
135 );
136 \register_block_type_from_metadata(
137 ACTIVITYPUB_PLUGIN_DIR . '/build/follow-me',
138 array(
139 'render_callback' => array( self::class, 'render_follow_me_block' ),
140 )
141 );
142 \register_block_type_from_metadata(
143 ACTIVITYPUB_PLUGIN_DIR . '/build/reply',
144 array(
145 'render_callback' => array( self::class, 'render_reply_block' ),
146 )
147 );
148
149 \register_block_type_from_metadata(
150 ACTIVITYPUB_PLUGIN_DIR . '/build/reactions',
151 array(
152 'render_callback' => array( self::class, 'render_post_reactions_block' ),
153 )
154 );
155 }
156
157 /**
158 * Render the post reactions block.
159 *
160 * @param array $attrs The block attributes.
161 *
162 * @return string The HTML to render.
163 */
164 public static function render_post_reactions_block( $attrs ) {
165 if ( ! isset( $attrs['postId'] ) ) {
166 $attrs['postId'] = get_the_ID();
167 }
168
169 $wrapper_attributes = get_block_wrapper_attributes(
170 array(
171 'class' => 'activitypub-reactions-block',
172 'data-attrs' => wp_json_encode( $attrs ),
173 )
174 );
175
176 return sprintf(
177 '<div %s></div>',
178 $wrapper_attributes
179 );
180 }
181
182 /**
183 * Get the user ID from a user string.
184 *
185 * @param string $user_string The user string. Can be a user ID, 'site', or 'inherit'.
186 * @return int|null The user ID, or null if the 'inherit' string is not supported in this context.
187 */
188 private static function get_user_id( $user_string ) {
189 if ( is_numeric( $user_string ) ) {
190 return absint( $user_string );
191 }
192
193 // If the user string is 'site', return the Blog User ID.
194 if ( 'site' === $user_string ) {
195 return Actors::BLOG_USER_ID;
196 }
197
198 // The only other value should be 'inherit', which means to use the query context to determine the User.
199 if ( 'inherit' !== $user_string ) {
200 return null;
201 }
202
203 // For a homepage/front page, if the Blog User is active, use it.
204 if ( ( is_front_page() || is_home() ) && ! is_user_type_disabled( 'blog' ) ) {
205 return Actors::BLOG_USER_ID;
206 }
207
208 // If we're in a loop, use the post author.
209 $author_id = get_the_author_meta( 'ID' );
210 if ( $author_id ) {
211 return $author_id;
212 }
213
214 // For other pages, the queried object will clue us in.
215 $queried_object = get_queried_object();
216 if ( ! $queried_object ) {
217 return null;
218 }
219
220 // If we're on a user archive page, use that user's ID.
221 if ( is_a( $queried_object, 'WP_User' ) ) {
222 return $queried_object->ID;
223 }
224
225 // For a single post, use the post author's ID.
226 if ( is_a( $queried_object, 'WP_Post' ) ) {
227 return get_the_author_meta( 'ID' );
228 }
229
230 // We won't properly account for some conditions, like tag archives.
231 return null;
232 }
233
234 /**
235 * Filter an array by a list of keys.
236 *
237 * @param array $data The array to filter.
238 * @param array $keys The keys to keep.
239 * @return array The filtered array.
240 */
241 protected static function filter_array_by_keys( $data, $keys ) {
242 return array_intersect_key( $data, array_flip( $keys ) );
243 }
244
245 /**
246 * Render the follow me block.
247 *
248 * @param array $attrs The block attributes.
249 * @return string The HTML to render.
250 */
251 public static function render_follow_me_block( $attrs ) {
252 $user_id = self::get_user_id( $attrs['selectedUser'] );
253 $user = Actors::get_by_id( $user_id );
254 if ( is_wp_error( $user ) ) {
255 if ( 'inherit' === $attrs['selectedUser'] ) {
256 // If the user is 'inherit' and we couldn't determine the user, don't render anything.
257 return '<!-- Follow Me block: `inherit` mode does not display on this type of page -->';
258 } else {
259 // If the user is a specific ID and we couldn't find it, render an error message.
260 return '<!-- Follow Me block: user not found -->';
261 }
262 }
263
264 $attrs['profileData'] = self::filter_array_by_keys(
265 $user->to_array(),
266 array( 'icon', 'name', 'webfinger' )
267 );
268
269 $wrapper_attributes = get_block_wrapper_attributes(
270 array(
271 'class' => 'activitypub-follow-me-block-wrapper',
272 'data-attrs' => wp_json_encode( $attrs ),
273 )
274 );
275 // todo: render more than an empty div?
276 return '<div ' . $wrapper_attributes . '></div>';
277 }
278
279 /**
280 * Render the follower block.
281 *
282 * @param array $attrs The block attributes.
283 *
284 * @return string The HTML to render.
285 */
286 public static function render_follower_block( $attrs ) {
287 $followee_user_id = self::get_user_id( $attrs['selectedUser'] );
288 if ( is_null( $followee_user_id ) ) {
289 return '<!-- Followers block: `inherit` mode does not display on this type of page -->';
290 }
291
292 $user = Actors::get_by_id( $followee_user_id );
293 if ( is_wp_error( $user ) ) {
294 return '<!-- Followers block: `' . $followee_user_id . '` not an active ActivityPub user -->';
295 }
296
297 $per_page = absint( $attrs['per_page'] );
298 $follower_data = Followers::get_followers_with_count( $followee_user_id, $per_page );
299
300 $attrs['followerData']['total'] = $follower_data['total'];
301 $attrs['followerData']['followers'] = array_map(
302 function ( $follower ) {
303 return self::filter_array_by_keys(
304 $follower->to_array(),
305 array( 'icon', 'name', 'preferredUsername', 'url' )
306 );
307 },
308 $follower_data['followers']
309 );
310 $wrapper_attributes = get_block_wrapper_attributes(
311 array(
312 'aria-label' => __( 'Fediverse Followers', 'activitypub' ),
313 'class' => 'activitypub-follower-block',
314 'data-attrs' => wp_json_encode( $attrs ),
315 )
316 );
317
318 $html = '<div ' . $wrapper_attributes . '>';
319 if ( $attrs['title'] ) {
320 $html .= '<h3>' . esc_html( $attrs['title'] ) . '</h3>';
321 }
322 $html .= '<ul>';
323 foreach ( $follower_data['followers'] as $follower ) {
324 $html .= '<li>' . self::render_follower( $follower ) . '</li>';
325 }
326 // We are only pagination on the JS side. Could be revisited but we gotta ship!
327 $html .= '</ul></div>';
328 return $html;
329 }
330
331 /**
332 * Render the reply block.
333 *
334 * @param array $attrs The block attributes.
335 *
336 * @return string The HTML to render.
337 */
338 public static function render_reply_block( $attrs ) {
339 $html = '';
340
341 if ( ! empty( $attrs['url'] ) ) {
342 $html = sprintf(
343 '<p><a title="%2$s" aria-label="%2$s" href="%1$s" class="u-in-reply-to" target="_blank">%3$s</a></p>',
344 esc_url( $attrs['url'] ),
345 esc_attr__( 'This post is a response to the referenced content.', 'activitypub' ),
346 // translators: %s is the URL of the post being replied to.
347 sprintf( __( '&#8620;%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', esc_url( $attrs['url'] ) ) )
348 );
349 }
350
351 /**
352 * Filter the reply block.
353 *
354 * @param string $html The HTML to render.
355 * @param array $attrs The block attributes.
356 */
357 return apply_filters( 'activitypub_reply_block', $html, $attrs );
358 }
359
360 /**
361 * Render a follower.
362 *
363 * @param \Activitypub\Model\Follower $follower The follower to render.
364 *
365 * @return string The HTML to render.
366 */
367 public static function render_follower( $follower ) {
368 $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>';
369 $template =
370 '<a href="%s" title="%s" class="components-external-link activitypub-link" target="_blank" rel="external noreferrer noopener">
371 <img width="40" height="40" src="%s" class="avatar activitypub-avatar" />
372 <span class="activitypub-actor">
373 <strong class="activitypub-name">%s</strong>
374 <span class="sep">/</span>
375 <span class="activitypub-handle">@%s</span>
376 </span>
377 %s
378 </a>';
379
380 $data = $follower->to_array();
381
382 return sprintf(
383 $template,
384 esc_url( object_to_uri( $data['url'] ) ),
385 esc_attr( $data['name'] ),
386 esc_attr( $data['icon']['url'] ),
387 esc_html( $data['name'] ),
388 esc_html( $data['preferredUsername'] ),
389 $external_svg
390 );
391 }
392 }
393