PluginProbe
ActivityPub / 7.0.0
ActivityPub v7.0.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 7.0.0, at includes/class-blocks.php

375 lines 11.3 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\Actors;
11
12 /**
13 * Block class.
14 */
15 class Blocks {
16 /**
17 * Initialize the class, registering WordPress hooks.
18 */
19 public static function init() {
20 // This is already being called on the init hook, so just add it.
21 self::register_blocks();
22
23 \add_action( 'load-post-new.php', array( self::class, 'handle_in_reply_to_get_param' ) );
24 // Add editor plugin.
25 \add_action( 'enqueue_block_editor_assets', array( self::class, 'enqueue_editor_assets' ) );
26 \add_action( 'init', array( self::class, 'register_postmeta' ), 11 );
27 \add_action( 'rest_api_init', array( self::class, 'register_rest_fields' ) );
28
29 \add_filter( 'activitypub_import_mastodon_post_data', array( self::class, 'filter_import_mastodon_post_data' ), 10, 2 );
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' => 'sanitize_text_field',
46 )
47 );
48
49 \register_post_meta(
50 $post_type,
51 'activitypub_content_visibility',
52 array(
53 'type' => 'string',
54 'single' => true,
55 'show_in_rest' => true,
56 'sanitize_callback' => function ( $value ) {
57 $schema = array(
58 'type' => 'string',
59 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ),
60 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC,
61 );
62
63 if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
64 return $schema['default'];
65 }
66
67 return $value;
68 },
69 )
70 );
71
72 \register_post_meta(
73 $post_type,
74 'activitypub_max_image_attachments',
75 array(
76 'type' => 'integer',
77 'single' => true,
78 'show_in_rest' => true,
79 'default' => \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ),
80 'sanitize_callback' => 'absint',
81 )
82 );
83 }
84 }
85
86 /**
87 * Enqueue the block editor assets.
88 */
89 public static function enqueue_editor_assets() {
90 $data = array(
91 'namespace' => ACTIVITYPUB_REST_NAMESPACE,
92 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg',
93 'enabled' => array(
94 'blog' => ! is_user_type_disabled( 'blog' ),
95 'users' => ! is_user_type_disabled( 'user' ),
96 ),
97 );
98 wp_localize_script( 'wp-editor', '_activityPubOptions', $data );
99
100 // Check for our supported post types.
101 $current_screen = \get_current_screen();
102 $ap_post_types = \get_post_types_by_support( 'activitypub' );
103 if ( ! $current_screen || ! in_array( $current_screen->post_type, $ap_post_types, true ) ) {
104 return;
105 }
106
107 $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/editor-plugin/plugin.asset.php';
108 $plugin_url = plugins_url( 'build/editor-plugin/plugin.js', ACTIVITYPUB_PLUGIN_FILE );
109 wp_enqueue_script( 'activitypub-block-editor', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true );
110 }
111
112 /**
113 * Enqueue the reply handle script if the in_reply_to GET param is set.
114 */
115 public static function handle_in_reply_to_get_param() {
116 // Only load the script if the in_reply_to GET param is set, action happens there, not here.
117 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
118 if ( ! isset( $_GET['in_reply_to'] ) ) {
119 return;
120 }
121
122 $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/reply-intent/plugin.asset.php';
123 $plugin_url = plugins_url( 'build/reply-intent/plugin.js', ACTIVITYPUB_PLUGIN_FILE );
124 wp_enqueue_script( 'activitypub-reply-intent', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true );
125 }
126
127 /**
128 * Register the blocks.
129 */
130 public static function register_blocks() {
131 \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/follow-me' );
132 \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/followers' );
133 \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/reactions' );
134
135 \register_block_type_from_metadata(
136 ACTIVITYPUB_PLUGIN_DIR . '/build/reply',
137 array(
138 'render_callback' => array( self::class, 'render_reply_block' ),
139 )
140 );
141 }
142
143 /**
144 * Register REST fields needed for blocks.
145 */
146 public static function register_rest_fields() {
147 // Register the post_count field for Follow Me block.
148 register_rest_field(
149 'user',
150 'post_count',
151 array(
152 /**
153 * Get the number of published posts.
154 *
155 * @param array $response Prepared response array.
156 * @param string $field_name The field name.
157 * @param \WP_REST_Request $request The request object.
158 * @return int The number of published posts.
159 */
160 'get_callback' => function ( $response, $field_name, $request ) {
161 return (int) count_user_posts( $request->get_param( 'id' ), 'post', true );
162 },
163 'schema' => array(
164 'description' => 'Number of published posts',
165 'type' => 'integer',
166 'context' => array( 'activitypub' ),
167 ),
168 )
169 );
170 }
171
172 /**
173 * Get the user ID from a user string.
174 *
175 * @param string $user_string The user string. Can be a user ID, 'blog', or 'inherit'.
176 * @return int|null The user ID, or null if the 'inherit' string is not supported in this context.
177 */
178 public static function get_user_id( $user_string ) {
179 if ( is_numeric( $user_string ) ) {
180 return absint( $user_string );
181 }
182
183 // If the user string is 'blog', return the Blog User ID.
184 if ( 'blog' === $user_string ) {
185 return Actors::BLOG_USER_ID;
186 }
187
188 // The only other value should be 'inherit', which means to use the query context to determine the User.
189 if ( 'inherit' !== $user_string ) {
190 return null;
191 }
192
193 // For a homepage/front page, if the Blog User is active, use it.
194 if ( ( is_front_page() || is_home() ) && ! is_user_type_disabled( 'blog' ) ) {
195 return Actors::BLOG_USER_ID;
196 }
197
198 // If we're in a loop, use the post author.
199 $author_id = get_the_author_meta( 'ID' );
200 if ( $author_id ) {
201 return $author_id;
202 }
203
204 // For other pages, the queried object will clue us in.
205 $queried_object = get_queried_object();
206 if ( ! $queried_object ) {
207 return null;
208 }
209
210 // If we're on a user archive page, use that user's ID.
211 if ( is_a( $queried_object, 'WP_User' ) ) {
212 return $queried_object->ID;
213 }
214
215 // For a single post, use the post author's ID.
216 if ( is_a( $queried_object, 'WP_Post' ) ) {
217 return get_the_author_meta( 'ID' );
218 }
219
220 // We won't properly account for some conditions, like tag archives.
221 return null;
222 }
223
224 /**
225 * Render the reply block.
226 *
227 * @param array $attrs The block attributes.
228 *
229 * @return string The HTML to render.
230 */
231 public static function render_reply_block( $attrs ) {
232 // Return early if no URL is provided.
233 if ( empty( $attrs['url'] ) ) {
234 return null;
235 }
236
237 $show_embed = isset( $attrs['embedPost'] ) && $attrs['embedPost'];
238
239 $wrapper_attrs = get_block_wrapper_attributes(
240 array(
241 'aria-label' => __( 'Reply', 'activitypub' ),
242 'class' => 'activitypub-reply-block',
243 'data-in-reply-to' => $attrs['url'],
244 )
245 );
246
247 $html = '<div ' . $wrapper_attrs . '>';
248
249 // Try to get and append the embed if requested.
250 if ( $show_embed ) {
251 $embed = wp_oembed_get( $attrs['url'] );
252 if ( $embed ) {
253 $html .= $embed;
254 }
255 }
256
257 // Only show the link if we're not showing the embed.
258 if ( ! $show_embed ) {
259 $html .= sprintf(
260 '<p><a title="%2$s" aria-label="%2$s" href="%1$s" class="u-in-reply-to" target="_blank">%3$s</a></p>',
261 esc_url( $attrs['url'] ),
262 esc_attr__( 'This post is a response to the referenced content.', 'activitypub' ),
263 // translators: %s is the URL of the post being replied to.
264 sprintf( __( '&#8620;%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', esc_url( $attrs['url'] ) ) )
265 );
266 }
267
268 $html .= '</div>';
269
270 return $html;
271 }
272
273 /**
274 * Renders a modal component that can be used by different blocks.
275 *
276 * @param array $args Arguments for the modal.
277 */
278 public static function render_modal( $args = array() ) {
279 $defaults = array(
280 'content' => '',
281 'is_compact' => false,
282 'title' => '',
283 );
284
285 $args = \wp_parse_args( $args, $defaults );
286 ?>
287
288 <div
289 class="activitypub-modal__overlay<?php echo \esc_attr( $args['is_compact'] ? ' compact' : '' ); ?>"
290 data-wp-bind--hidden="!context.modal.isOpen"
291 data-wp-watch="callbacks.handleModalEffects"
292 role="dialog"
293 aria-modal="true"
294 hidden
295 >
296 <div class="activitypub-modal__frame">
297 <?php if ( ! $args['is_compact'] || ! empty( $args['title'] ) ) : ?>
298 <div class="activitypub-modal__header">
299 <h2 class="activitypub-modal__title"><?php echo \esc_html( $args['title'] ); ?></h2>
300 <button
301 type="button"
302 class="activitypub-modal__close wp-element-button wp-block-button__link"
303 data-wp-on--click="actions.closeModal"
304 aria-label="<?php echo \esc_attr__( 'Close dialog', 'activitypub' ); ?>"
305 >
306 <svg fill="currentColor" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
307 <path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path>
308 </svg>
309 </button>
310 </div>
311 <?php endif; ?>
312 <div class="activitypub-modal__content">
313 <?php echo $args['content']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
314 </div>
315 </div>
316 </div>
317 <?php
318 }
319
320 /**
321 * Converts content to blocks before saving to the database.
322 *
323 * @param array $data The post data to be inserted.
324 * @param object $post The Mastodon Create activity.
325 *
326 * @return array
327 */
328 public static function filter_import_mastodon_post_data( $data, $post ) {
329 // Convert paragraphs to blocks.
330 \preg_match_all( '#<p>.*?</p>#is', $data['post_content'], $matches );
331 $blocks = \array_map(
332 function ( $paragraph ) {
333 return '<!-- wp:paragraph -->' . PHP_EOL . $paragraph . PHP_EOL . '<!-- /wp:paragraph -->' . PHP_EOL;
334 },
335 $matches[0] ?? array()
336 );
337
338 $data['post_content'] = \rtrim( \implode( PHP_EOL, $blocks ), PHP_EOL );
339
340 // Add reply block if it's a reply.
341 if ( null !== $post->object->inReplyTo ) {
342 $reply_block = \sprintf( '<!-- wp:activitypub/reply {"url":"%1$s","embedPost":true} /-->' . PHP_EOL, \esc_url( $post->object->inReplyTo ) );
343 $data['post_content'] = $reply_block . $data['post_content'];
344 }
345
346 return $data;
347 }
348
349 /**
350 * Add Interactivity directions to the specified element.
351 *
352 * @param string $content The block content.
353 * @param string[] $selector The selector for the element to add directions to.
354 * @param string[] $attributes The attributes to add to the element.
355 *
356 * @return string The updated content.
357 */
358 public static function add_directions( $content, $selector, $attributes ) {
359 $tags = new \WP_HTML_Tag_Processor( $content );
360
361 while ( $tags->next_tag( $selector ) ) {
362 foreach ( $attributes as $key => $value ) {
363 if ( 'class' === $key ) {
364 $tags->add_class( $value );
365 continue;
366 }
367
368 $tags->set_attribute( $key, $value );
369 }
370 }
371
372 return $tags->get_updated_html();
373 }
374 }
375