PluginProbe
ActivityPub / 4.2.1
ActivityPub v4.2.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.2.1, at includes/class-blocks.php

361 lines 11.2 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 as User_Collection;
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, 'add_data' ) );
25 \add_action( 'enqueue_block_editor_assets', array( self::class, 'add_data' ) );
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 * Add data to the block editor.
110 */
111 public static function add_data() {
112 $context = is_admin() ? 'editor' : 'view';
113 $followers_handle = 'activitypub-followers-' . $context . '-script';
114 $follow_me_handle = 'activitypub-follow-me-' . $context . '-script';
115 $data = array(
116 'namespace' => ACTIVITYPUB_REST_NAMESPACE,
117 'enabled' => array(
118 'site' => ! is_user_type_disabled( 'blog' ),
119 'users' => ! is_user_type_disabled( 'user' ),
120 ),
121 );
122 $js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) );
123 \wp_add_inline_script( $followers_handle, $js, 'before' );
124 \wp_add_inline_script( $follow_me_handle, $js, 'before' );
125 }
126
127 /**
128 * Register the blocks.
129 */
130 public static function register_blocks() {
131 \register_block_type_from_metadata(
132 ACTIVITYPUB_PLUGIN_DIR . '/build/followers',
133 array(
134 'render_callback' => array( self::class, 'render_follower_block' ),
135 )
136 );
137 \register_block_type_from_metadata(
138 ACTIVITYPUB_PLUGIN_DIR . '/build/follow-me',
139 array(
140 'render_callback' => array( self::class, 'render_follow_me_block' ),
141 )
142 );
143 \register_block_type_from_metadata(
144 ACTIVITYPUB_PLUGIN_DIR . '/build/reply',
145 array(
146 'render_callback' => array( self::class, 'render_reply_block' ),
147 )
148 );
149 }
150
151 /**
152 * Get the user ID from a user string.
153 *
154 * @param string $user_string The user string. Can be a user ID, 'site', or 'inherit'.
155 * @return int|null The user ID, or null if the 'inherit' string is not supported in this context.
156 */
157 private static function get_user_id( $user_string ) {
158 if ( is_numeric( $user_string ) ) {
159 return absint( $user_string );
160 }
161
162 // If the user string is 'site', return the Blog User ID.
163 if ( 'site' === $user_string ) {
164 return User_Collection::BLOG_USER_ID;
165 }
166
167 // The only other value should be 'inherit', which means to use the query context to determine the User.
168 if ( 'inherit' !== $user_string ) {
169 return null;
170 }
171
172 // For a homepage/front page, if the Blog User is active, use it.
173 if ( ( is_front_page() || is_home() ) && ! is_user_type_disabled( 'blog' ) ) {
174 return User_Collection::BLOG_USER_ID;
175 }
176
177 // If we're in a loop, use the post author.
178 $author_id = get_the_author_meta( 'ID' );
179 if ( $author_id ) {
180 return $author_id;
181 }
182
183 // For other pages, the queried object will clue us in.
184 $queried_object = get_queried_object();
185 if ( ! $queried_object ) {
186 return null;
187 }
188
189 // If we're on a user archive page, use that user's ID.
190 if ( is_a( $queried_object, 'WP_User' ) ) {
191 return $queried_object->ID;
192 }
193
194 // For a single post, use the post author's ID.
195 if ( is_a( $queried_object, 'WP_Post' ) ) {
196 return get_the_author_meta( 'ID' );
197 }
198
199 // We won't properly account for some conditions, like tag archives.
200 return null;
201 }
202
203 /**
204 * Filter an array by a list of keys.
205 *
206 * @param array $data The array to filter.
207 * @param array $keys The keys to keep.
208 * @return array The filtered array.
209 */
210 protected static function filter_array_by_keys( $data, $keys ) {
211 return array_intersect_key( $data, array_flip( $keys ) );
212 }
213
214 /**
215 * Render the follow me block.
216 *
217 * @param array $attrs The block attributes.
218 * @return string The HTML to render.
219 */
220 public static function render_follow_me_block( $attrs ) {
221 $user_id = self::get_user_id( $attrs['selectedUser'] );
222 $user = User_Collection::get_by_id( $user_id );
223 if ( is_wp_error( $user ) ) {
224 if ( 'inherit' === $attrs['selectedUser'] ) {
225 // If the user is 'inherit' and we couldn't determine the user, don't render anything.
226 return '<!-- Follow Me block: `inherit` mode does not display on this type of page -->';
227 } else {
228 // If the user is a specific ID and we couldn't find it, render an error message.
229 return '<!-- Follow Me block: user not found -->';
230 }
231 }
232
233 $attrs['profileData'] = self::filter_array_by_keys(
234 $user->to_array(),
235 array( 'icon', 'name', 'webfinger' )
236 );
237
238 $wrapper_attributes = get_block_wrapper_attributes(
239 array(
240 'aria-label' => __( 'Follow me on the Fediverse', 'activitypub' ),
241 'class' => 'activitypub-follow-me-block-wrapper',
242 'data-attrs' => wp_json_encode( $attrs ),
243 )
244 );
245 // todo: render more than an empty div?
246 return '<div ' . $wrapper_attributes . '></div>';
247 }
248
249 /**
250 * Render the follower block.
251 *
252 * @param array $attrs The block attributes.
253 *
254 * @return string The HTML to render.
255 */
256 public static function render_follower_block( $attrs ) {
257 $followee_user_id = self::get_user_id( $attrs['selectedUser'] );
258 if ( is_null( $followee_user_id ) ) {
259 return '<!-- Followers block: `inherit` mode does not display on this type of page -->';
260 }
261
262 $user = User_Collection::get_by_id( $followee_user_id );
263 if ( is_wp_error( $user ) ) {
264 return '<!-- Followers block: `' . $followee_user_id . '` not an active ActivityPub user -->';
265 }
266
267 $per_page = absint( $attrs['per_page'] );
268 $follower_data = Followers::get_followers_with_count( $followee_user_id, $per_page );
269
270 $attrs['followerData']['total'] = $follower_data['total'];
271 $attrs['followerData']['followers'] = array_map(
272 function ( $follower ) {
273 return self::filter_array_by_keys(
274 $follower->to_array(),
275 array( 'icon', 'name', 'preferredUsername', 'url' )
276 );
277 },
278 $follower_data['followers']
279 );
280 $wrapper_attributes = get_block_wrapper_attributes(
281 array(
282 'aria-label' => __( 'Fediverse Followers', 'activitypub' ),
283 'class' => 'activitypub-follower-block',
284 'data-attrs' => wp_json_encode( $attrs ),
285 )
286 );
287
288 $html = '<div ' . $wrapper_attributes . '>';
289 if ( $attrs['title'] ) {
290 $html .= '<h3>' . esc_html( $attrs['title'] ) . '</h3>';
291 }
292 $html .= '<ul>';
293 foreach ( $follower_data['followers'] as $follower ) {
294 $html .= '<li>' . self::render_follower( $follower ) . '</li>';
295 }
296 // We are only pagination on the JS side. Could be revisited but we gotta ship!
297 $html .= '</ul></div>';
298 return $html;
299 }
300
301 /**
302 * Render the reply block.
303 *
304 * @param array $attrs The block attributes.
305 *
306 * @return string The HTML to render.
307 */
308 public static function render_reply_block( $attrs ) {
309 /**
310 * Filter the reply block.
311 *
312 * @param string $html The HTML to render.
313 * @param array $attrs The block attributes.
314 */
315 return apply_filters(
316 'activitypub_reply_block',
317 sprintf(
318 '<p><a title="%2$s" aria-label="%2$s" href="%1$s" class="u-in-reply-to" target="_blank">%3$s</a></p>',
319 esc_url( $attrs['url'] ),
320 esc_attr__( 'This post is a response to the referenced content.', 'activitypub' ),
321 // translators: %s is the URL of the post being replied to.
322 sprintf( __( '&#8620;%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', $attrs['url'] ) )
323 ),
324 $attrs
325 );
326 }
327
328 /**
329 * Render a follower.
330 *
331 * @param \Activitypub\Model\Follower $follower The follower to render.
332 *
333 * @return string The HTML to render.
334 */
335 public static function render_follower( $follower ) {
336 $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>';
337 $template =
338 '<a href="%s" title="%s" class="components-external-link activitypub-link" target="_blank" rel="external noreferrer noopener">
339 <img width="40" height="40" src="%s" class="avatar activitypub-avatar" />
340 <span class="activitypub-actor">
341 <strong class="activitypub-name">%s</strong>
342 <span class="sep">/</span>
343 <span class="activitypub-handle">@%s</span>
344 </span>
345 %s
346 </a>';
347
348 $data = $follower->to_array();
349
350 return sprintf(
351 $template,
352 esc_url( object_to_uri( $data['url'] ) ),
353 esc_attr( $data['name'] ),
354 esc_attr( $data['icon']['url'] ),
355 esc_html( $data['name'] ),
356 esc_html( $data['preferredUsername'] ),
357 $external_svg
358 );
359 }
360 }
361