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-activitypub.php +628 -181 9.2.05.7.0 View file →
@@ -6,12 +6,13 @@
6 6 */
7 7
8 8 namespace Activitypub;
9 9
10 +use Exception;
11 +use Activitypub\Collection\Actors;
12 +use Activitypub\Collection\Outbox;
10 13 use Activitypub\Collection\Followers;
11 -use Activitypub\Collection\Following;
12 -use Activitypub\Collection\Remote_Posts;
13 -use Activitypub\OAuth\Client;
14 +use Activitypub\Collection\Extra_Fields;
14 15
15 16 /**
16 17 * ActivityPub Class.
17 18 *
@@ -21,62 +22,64 @@
21 22 /**
22 23 * Initialize the class, registering WordPress hooks.
23 24 */
24 25 public static function init() {
25 - \add_action( 'init', array( self::class, 'theme_compat' ), 11 );
26 - \add_action( 'init', array( self::class, 'register_user_meta' ), 11 );
26 + \add_filter( 'template_include', array( self::class, 'render_activitypub_template' ), 99 );
27 + \add_action( 'template_redirect', array( self::class, 'template_redirect' ) );
28 + \add_filter( 'redirect_canonical', array( self::class, 'redirect_canonical' ), 10, 2 );
29 + \add_filter( 'redirect_canonical', array( self::class, 'no_trailing_redirect' ), 10, 2 );
30 + \add_filter( 'query_vars', array( self::class, 'add_query_vars' ) );
31 + \add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
27 32
33 + // Add support for ActivityPub to custom post types.
34 + $post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) );
35 +
36 + foreach ( $post_types as $post_type ) {
37 + \add_post_type_support( $post_type, 'activitypub' );
38 + }
39 +
28 40 \add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 );
29 41 \add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 );
30 42
43 + \add_action( 'init', array( self::class, 'add_rewrite_rules' ), 11 );
44 + \add_action( 'init', array( self::class, 'theme_compat' ), 11 );
45 +
31 46 \add_action( 'user_register', array( self::class, 'user_register' ) );
32 47
33 - \add_action( 'activitypub_add_user_block', array( Followers::class, 'remove_blocked_actors' ), 10, 3 );
34 - \add_action( 'activitypub_add_user_block', array( Following::class, 'remove_blocked_actors' ), 10, 3 );
48 + \add_filter( 'activitypub_get_actor_extra_fields', array( Extra_Fields::class, 'default_actor_extra_fields' ), 10, 2 );
49 +
50 + \add_action( 'updated_postmeta', array( self::class, 'updated_postmeta' ), 10, 4 );
51 + \add_action( 'added_post_meta', array( self::class, 'updated_postmeta' ), 10, 4 );
52 +
53 + \add_action( 'init', array( self::class, 'register_user_meta' ), 11 );
54 +
55 + // Register several post_types.
56 + self::register_post_types();
57 +
58 + self::register_oembed_providers();
59 + Embed::init();
35 60 }
36 61
37 62 /**
38 63 * Activation Hook.
39 - *
40 - * @param bool $network_wide Whether to activate the plugin for all sites in the network or just the current site.
41 64 */
42 - public static function activate( $network_wide ) {
65 + public static function activate() {
43 66 self::flush_rewrite_rules();
44 67 Scheduler::register_schedules();
45 68
46 - \add_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ), 5, 3 );
69 + \add_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ), 10, 3 );
47 70 Migration::update_comment_counts();
48 -
49 - if ( \is_multisite() && $network_wide && ! \wp_is_large_network() ) {
50 - $sites = \get_sites( array( 'fields' => 'ids' ) );
51 - foreach ( $sites as $site ) {
52 - \switch_to_blog( $site );
53 - self::flush_rewrite_rules();
54 - \restore_current_blog();
55 - }
56 - }
57 71 }
58 72
59 73 /**
60 74 * Deactivation Hook.
61 - *
62 - * @param bool $network_wide Whether to deactivate the plugin for all sites in the network or just the current site.
63 75 */
64 - public static function deactivate( $network_wide ) {
76 + public static function deactivate() {
65 77 self::flush_rewrite_rules();
66 78 Scheduler::deregister_schedules();
67 79
68 - \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ), 5 );
80 + \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) );
69 81 Migration::update_comment_counts( 2000 );
70 -
71 - if ( \is_multisite() && $network_wide && ! \wp_is_large_network() ) {
72 - $sites = \get_sites( array( 'fields' => 'ids' ) );
73 - foreach ( $sites as $site ) {
74 - \switch_to_blog( $site );
75 - self::flush_rewrite_rules();
76 - \restore_current_blog();
77 - }
78 - }
79 82 }
80 83
81 84 /**
82 85 * Uninstall Hook.
@@ -83,19 +86,313 @@
83 86 */
84 87 public static function uninstall() {
85 88 Scheduler::deregister_schedules();
86 89
87 - \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ), 5 );
90 + \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) );
88 91 Migration::update_comment_counts( 2000 );
89 92
90 - Remote_Posts::delete_all();
91 - Tombstone::delete_all();
92 - Client::delete_all();
93 + \delete_option( 'activitypub_actor_mode' );
94 + \delete_option( 'activitypub_allow_likes' );
95 + \delete_option( 'activitypub_allow_replies' );
96 + \delete_option( 'activitypub_attribution_domains' );
97 + \delete_option( 'activitypub_authorized_fetch' );
98 + \delete_option( 'activitypub_application_user_private_key' );
99 + \delete_option( 'activitypub_application_user_public_key' );
100 + \delete_option( 'activitypub_blog_user_also_known_as' );
101 + \delete_option( 'activitypub_blog_user_moved_to' );
102 + \delete_option( 'activitypub_blog_user_private_key' );
103 + \delete_option( 'activitypub_blog_user_public_key' );
104 + \delete_option( 'activitypub_blog_description' );
105 + \delete_option( 'activitypub_blog_identifier' );
106 + \delete_option( 'activitypub_custom_post_content' );
107 + \delete_option( 'activitypub_db_version' );
108 + \delete_option( 'activitypub_default_extra_fields' );
109 + \delete_option( 'activitypub_enable_blog_user' );
110 + \delete_option( 'activitypub_enable_users' );
111 + \delete_option( 'activitypub_header_image' );
112 + \delete_option( 'activitypub_last_post_with_permalink_as_id' );
113 + \delete_option( 'activitypub_mailer_new_follower' );
114 + \delete_option( 'activitypub_mailer_new_dm' );
115 + \delete_option( 'activitypub_max_image_attachments' );
116 + \delete_option( 'activitypub_migration_lock' );
117 + \delete_option( 'activitypub_object_type' );
118 + \delete_option( 'activitypub_outbox_purge_days' );
119 + \delete_option( 'activitypub_shared_inbox' );
120 + \delete_option( 'activitypub_support_post_types' );
121 + \delete_option( 'activitypub_use_hashtags' );
122 + \delete_option( 'activitypub_use_opengraph' );
123 + \delete_option( 'activitypub_use_permalink_as_id_for_blog' );
124 + \delete_option( 'activitypub_vary_header' );
125 + }
93 126
94 - Options::delete();
127 + /**
128 + * Return a AS2 JSON version of an author, post or page.
129 + *
130 + * @param string $template The path to the template object.
131 + *
132 + * @return string The new path to the JSON template.
133 + */
134 + public static function render_activitypub_template( $template ) {
135 + if ( \wp_is_serving_rest_request() || \wp_doing_ajax() ) {
136 + return $template;
137 + }
138 +
139 + self::add_headers();
140 +
141 + if ( ! is_activitypub_request() ) {
142 + return $template;
143 + }
144 +
145 + $activitypub_template = false;
146 + $activitypub_object = Query::get_instance()->get_activitypub_object();
147 +
148 + if ( $activitypub_object ) {
149 + if ( \get_query_var( 'preview' ) ) {
150 + \define( 'ACTIVITYPUB_PREVIEW', true );
151 +
152 + /**
153 + * Filter the template used for the ActivityPub preview.
154 + *
155 + * @param string $activitypub_template Absolute path to the template file.
156 + */
157 + $activitypub_template = apply_filters( 'activitypub_preview_template', ACTIVITYPUB_PLUGIN_DIR . '/templates/post-preview.php' );
158 + } else {
159 + $activitypub_template = ACTIVITYPUB_PLUGIN_DIR . 'templates/activitypub-json.php';
160 + }
161 + }
162 +
163 + /*
164 + * Check if the request is authorized.
165 + *
166 + * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
167 + * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
168 + */
169 + if ( $activitypub_template && use_authorized_fetch() ) {
170 + $verification = Signature::verify_http_signature( $_SERVER );
171 + if ( \is_wp_error( $verification ) ) {
172 + header( 'HTTP/1.1 401 Unauthorized' );
173 +
174 + // Fallback as template_loader can't return http headers.
175 + return $template;
176 + }
177 + }
178 +
179 + if ( $activitypub_template ) {
180 + \set_query_var( 'is_404', false );
181 +
182 + // Check if header already sent.
183 + if ( ! \headers_sent() ) {
184 + // Send 200 status header.
185 + \status_header( 200 );
186 + }
187 +
188 + return $activitypub_template;
189 + }
190 +
191 + return $template;
95 192 }
96 193
97 194 /**
195 + * Add the 'self' link to the header.
196 + */
197 + public static function add_headers() {
198 + $id = Query::get_instance()->get_activitypub_object_id();
199 +
200 + if ( ! $id ) {
201 + return;
202 + }
203 +
204 + if ( ! headers_sent() ) {
205 + \header( 'Link: <' . esc_url( $id ) . '>; title="ActivityPub (JSON)"; rel="alternate"; type="application/activity+json"', false );
206 +
207 + if ( \get_option( 'activitypub_vary_header' ) ) {
208 + // Send Vary header for Accept header.
209 + \header( 'Vary: Accept', false );
210 + }
211 + }
212 +
213 + add_action(
214 + 'wp_head',
215 + function () use ( $id ) {
216 + echo PHP_EOL . '<link rel="alternate" title="ActivityPub (JSON)" type="application/activity+json" href="' . esc_url( $id ) . '" />' . PHP_EOL;
217 + }
218 + );
219 + }
220 +
221 + /**
222 + * Remove trailing slash from ActivityPub @username requests.
223 + *
224 + * @param string $redirect_url The URL to redirect to.
225 + * @param string $requested_url The requested URL.
226 + *
227 + * @return string $redirect_url The possibly-unslashed redirect URL.
228 + */
229 + public static function no_trailing_redirect( $redirect_url, $requested_url ) {
230 + if ( get_query_var( 'actor' ) ) {
231 + return $requested_url;
232 + }
233 +
234 + return $redirect_url;
235 + }
236 +
237 + /**
238 + * Add support for `p` and `author` query vars.
239 + *
240 + * @param string $redirect_url The URL to redirect to.
241 + * @param string $requested_url The requested URL.
242 + *
243 + * @return string $redirect_url
244 + */
245 + public static function redirect_canonical( $redirect_url, $requested_url ) {
246 + if ( ! is_activitypub_request() ) {
247 + return $redirect_url;
248 + }
249 +
250 + $query = \wp_parse_url( $requested_url, PHP_URL_QUERY );
251 +
252 + if ( ! $query ) {
253 + return $redirect_url;
254 + }
255 +
256 + $query_params = \wp_parse_args( $query );
257 + unset( $query_params['activitypub'] );
258 +
259 + if ( 1 !== count( $query_params ) ) {
260 + return $redirect_url;
261 + }
262 +
263 + if ( isset( $query_params['p'] ) ) {
264 + return null;
265 + }
266 +
267 + if ( isset( $query_params['author'] ) ) {
268 + return null;
269 + }
270 +
271 + return $requested_url;
272 + }
273 +
274 + /**
275 + * Custom redirects for ActivityPub requests.
276 + *
277 + * @return void
278 + */
279 + public static function template_redirect() {
280 + global $wp_query;
281 +
282 + $comment_id = get_query_var( 'c', null );
283 +
284 + // Check if it seems to be a comment.
285 + if ( $comment_id ) {
286 + $comment = get_comment( $comment_id );
287 +
288 + // Load a 404-page if `c` is set but not valid.
289 + if ( ! $comment ) {
290 + $wp_query->set_404();
291 + return;
292 + }
293 +
294 + // Stop if it's not an ActivityPub comment.
295 + if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
296 + return;
297 + }
298 +
299 + wp_safe_redirect( get_comment_link( $comment ) );
300 + exit;
301 + }
302 +
303 + $actor = get_query_var( 'actor', null );
304 + if ( $actor ) {
305 + $actor = Actors::get_by_username( $actor );
306 + if ( ! $actor || \is_wp_error( $actor ) ) {
307 + $wp_query->set_404();
308 + return;
309 + }
310 +
311 + if ( is_activitypub_request() ) {
312 + return;
313 + }
314 +
315 + if ( $actor->get__id() > 0 ) {
316 + $redirect_url = $actor->get_url();
317 + } else {
318 + $redirect_url = get_bloginfo( 'url' );
319 + }
320 +
321 + wp_safe_redirect( $redirect_url, 301 );
322 + exit;
323 + }
324 + }
325 +
326 + /**
327 + * Add the 'activitypub' query variable so WordPress won't mangle it.
328 + *
329 + * @param array $vars The query variables.
330 + *
331 + * @return array The query variables.
332 + */
333 + public static function add_query_vars( $vars ) {
334 + $vars[] = 'activitypub';
335 + $vars[] = 'preview';
336 + $vars[] = 'author';
337 + $vars[] = 'actor';
338 + $vars[] = 'c';
339 + $vars[] = 'p';
340 +
341 + return $vars;
342 + }
343 +
344 + /**
345 + * Replaces the default avatar.
346 + *
347 + * @param array $args Arguments passed to get_avatar_data(), after processing.
348 + * @param int|string|object $id_or_email A user ID, email address, or comment object.
349 + *
350 + * @return array $args
351 + */
352 + public static function pre_get_avatar_data( $args, $id_or_email ) {
353 + if (
354 + ! $id_or_email instanceof \WP_Comment ||
355 + ! isset( $id_or_email->comment_type ) ||
356 + $id_or_email->user_id
357 + ) {
358 + return $args;
359 + }
360 +
361 + $allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) );
362 + if (
363 + ! empty( $id_or_email->comment_type ) &&
364 + ! \in_array(
365 + $id_or_email->comment_type,
366 + (array) $allowed_comment_types,
367 + true
368 + )
369 + ) {
370 + $args['url'] = false;
371 + /** This filter is documented in wp-includes/link-template.php */
372 + return \apply_filters( 'get_avatar_data', $args, $id_or_email );
373 + }
374 +
375 + // Check if comment has an avatar.
376 + $avatar = \get_comment_meta( $id_or_email->comment_ID, 'avatar_url', true );
377 +
378 + if ( $avatar ) {
379 + if ( empty( $args['class'] ) ) {
380 + $args['class'] = array();
381 + } elseif ( \is_string( $args['class'] ) ) {
382 + $args['class'] = \explode( ' ', $args['class'] );
383 + }
384 +
385 + $args['url'] = $avatar;
386 + $args['class'][] = 'avatar-activitypub';
387 + $args['class'][] = 'u-photo';
388 + $args['class'] = \array_unique( $args['class'] );
389 + }
390 +
391 + return $args;
392 + }
393 +
394 + /**
98 395 * Store permalink in meta, to send delete Activity.
99 396 *
100 397 * @param string $post_id The Post ID.
101 398 */
@@ -117,24 +414,45 @@
117 414 \delete_post_meta( $post_id, '_activitypub_canonical_url' );
118 415 }
119 416
120 417 /**
121 - * Flush rewrite rules.
418 + * Add rewrite rules.
122 419 */
123 - public static function flush_rewrite_rules() {
124 - Router::add_rewrite_rules();
125 - \flush_rewrite_rules();
420 + public static function add_rewrite_rules() {
421 + /*
422 + * If another system needs to take precedence over the ActivityPub rewrite rules,
423 + * they can define their own and will manually call the appropriate functions as required.
424 + */
425 + if ( ACTIVITYPUB_DISABLE_REWRITES ) {
426 + return;
427 + }
428 +
429 + if ( ! \class_exists( 'Webfinger' ) ) {
430 + \add_rewrite_rule(
431 + '^.well-known/webfinger',
432 + 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger',
433 + 'top'
434 + );
435 + }
436 +
437 + if ( ! \class_exists( 'Nodeinfo_Endpoint' ) && true === (bool) \get_option( 'blog_public', 1 ) ) {
438 + \add_rewrite_rule(
439 + '^.well-known/nodeinfo',
440 + 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo',
441 + 'top'
442 + );
443 + }
444 +
445 + \add_rewrite_rule( '^@([\w\-\.]+)\/?$', 'index.php?actor=$matches[1]', 'top' );
446 + \add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
126 447 }
127 448
128 449 /**
129 - * Add rewrite rules.
130 - *
131 - * @deprecated 7.5.0 Use {@see Router::add_rewrite_rules()}.
450 + * Flush rewrite rules.
132 451 */
133 - public static function add_rewrite_rules() {
134 - \_deprecated_function( __FUNCTION__, '7.5.0', '\Activitypub\Router::add_rewrite_rules()' );
135 -
136 - Router::add_rewrite_rules();
452 + public static function flush_rewrite_rules() {
453 + self::add_rewrite_rules();
454 + \flush_rewrite_rules();
137 455 }
138 456
139 457 /**
140 458 * Theme compatibility stuff.
@@ -141,11 +459,11 @@
141 459 */
142 460 public static function theme_compat() {
143 461 // We assume that you want to use Post-Formats when enabling the setting.
144 462 if ( 'wordpress-post-format' === \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ) ) {
145 - if ( ! \get_theme_support( 'post-formats' ) ) {
463 + if ( ! get_theme_support( 'post-formats' ) ) {
146 464 // Add support for the Aside, Gallery Post Formats...
147 - \add_theme_support(
465 + add_theme_support(
148 466 'post-formats',
149 467 array(
150 468 'gallery',
151 469 'status',
@@ -158,185 +476,333 @@
158 476 }
159 477 }
160 478
161 479 /**
162 - * Add the 'activitypub' capability to users who can publish posts.
163 - *
164 - * @param int $user_id User ID.
480 + * Register Custom Post Types.
165 481 */
166 - public static function user_register( $user_id ) {
167 - if ( \user_can( $user_id, 'publish_posts' ) ) {
168 - $user = \get_user_by( 'id', $user_id );
169 - $user->add_cap( 'activitypub' );
170 - }
171 - }
482 + private static function register_post_types() {
483 + \register_post_type(
484 + Followers::POST_TYPE,
485 + array(
486 + 'labels' => array(
487 + 'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ),
488 + 'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ),
489 + ),
490 + 'public' => false,
491 + 'hierarchical' => false,
492 + 'rewrite' => false,
493 + 'query_var' => false,
494 + 'delete_with_user' => false,
495 + 'can_export' => true,
496 + 'supports' => array(),
497 + )
498 + );
172 499
173 - /**
174 - * Register user meta.
175 - */
176 - public static function register_user_meta() {
177 - $blog_prefix = $GLOBALS['wpdb']->get_blog_prefix();
178 -
179 - \register_meta(
180 - 'user',
181 - $blog_prefix . 'activitypub_also_known_as',
500 + \register_post_meta(
501 + Followers::POST_TYPE,
502 + '_activitypub_inbox',
182 503 array(
183 - 'type' => 'array',
184 - 'description' => 'An array of URLs that the user is known by.',
504 + 'type' => 'string',
185 505 'single' => true,
186 - 'default' => array(),
187 - 'sanitize_callback' => array( Sanitize::class, 'identifier_list' ),
506 + 'sanitize_callback' => 'sanitize_url',
188 507 )
189 508 );
190 509
191 - \register_meta(
192 - 'user',
193 - $blog_prefix . 'activitypub_hide_social_graph',
510 + \register_post_meta(
511 + Followers::POST_TYPE,
512 + '_activitypub_errors',
194 513 array(
195 - 'type' => 'integer',
196 - 'description' => 'Hide Followers and Followings on Profile.',
197 - 'single' => true,
198 - 'default' => 0,
199 - 'sanitize_callback' => 'absint',
200 - 'show_in_rest' => true,
514 + 'type' => 'string',
515 + 'single' => false,
516 + 'sanitize_callback' => function ( $value ) {
517 + if ( ! is_string( $value ) ) {
518 + throw new Exception( 'Error message is no valid string' );
519 + }
520 +
521 + return esc_sql( $value );
522 + },
201 523 )
202 524 );
203 525
204 - \register_meta(
205 - 'user',
206 - $blog_prefix . 'activitypub_old_host_data',
526 + \register_post_meta(
527 + Followers::POST_TYPE,
528 + '_activitypub_user_id',
207 529 array(
208 - 'description' => 'Actor object for the user on the old host.',
209 - 'single' => true,
530 + 'type' => 'string',
531 + 'single' => false,
532 + 'sanitize_callback' => function ( $value ) {
533 + return esc_sql( $value );
534 + },
210 535 )
211 536 );
212 537
213 - \register_meta(
214 - 'user',
215 - $blog_prefix . 'activitypub_moved_to',
538 + \register_post_meta(
539 + Followers::POST_TYPE,
540 + '_activitypub_actor_json',
216 541 array(
217 542 'type' => 'string',
218 - 'description' => 'The new URL of the user.',
219 543 'single' => true,
220 - 'sanitize_callback' => 'sanitize_url',
544 + 'sanitize_callback' => function ( $value ) {
545 + return sanitize_text_field( $value );
546 + },
221 547 )
222 548 );
223 549
224 - \register_meta(
225 - 'user',
226 - $blog_prefix . 'activitypub_description',
550 + // Register Outbox Post-Type.
551 + register_post_type(
552 + Outbox::POST_TYPE,
227 553 array(
554 + 'labels' => array(
555 + 'name' => _x( 'Outbox', 'post_type plural name', 'activitypub' ),
556 + 'singular_name' => _x( 'Outbox Item', 'post_type single name', 'activitypub' ),
557 + ),
558 + 'capabilities' => array(
559 + 'create_posts' => false,
560 + ),
561 + 'map_meta_cap' => true,
562 + 'public' => false,
563 + 'show_in_rest' => true,
564 + 'rewrite' => false,
565 + 'query_var' => false,
566 + 'supports' => array( 'title', 'editor', 'author', 'custom-fields' ),
567 + 'delete_with_user' => true,
568 + 'can_export' => true,
569 + 'exclude_from_search' => true,
570 + )
571 + );
572 +
573 + /**
574 + * Register Activity Type meta for Outbox items.
575 + *
576 + * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
577 + */
578 + \register_post_meta(
579 + Outbox::POST_TYPE,
580 + '_activitypub_activity_type',
581 + array(
228 582 'type' => 'string',
229 - 'description' => 'The user description.',
583 + 'description' => 'The type of the activity',
230 584 'single' => true,
231 - 'default' => '',
232 - 'sanitize_callback' => static function ( $value ) {
233 - return \wp_kses( $value, 'user_description' );
585 + 'show_in_rest' => true,
586 + 'sanitize_callback' => function ( $value ) {
587 + $value = ucfirst( strtolower( $value ) );
588 + $schema = array(
589 + 'type' => 'string',
590 + 'enum' => array( 'Accept', 'Add', 'Announce', 'Arrive', 'Block', 'Create', 'Delete', 'Dislike', 'Flag', 'Follow', 'Ignore', 'Invite', 'Join', 'Leave', 'Like', 'Listen', 'Move', 'Offer', 'Question', 'Reject', 'Read', 'Remove', 'TentativeReject', 'TentativeAccept', 'Travel', 'Undo', 'Update', 'View' ),
591 + 'default' => 'Announce',
592 + );
593 +
594 + if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
595 + return $schema['default'];
596 + }
597 +
598 + return $value;
234 599 },
235 600 )
236 601 );
237 602
238 - \register_meta(
239 - 'user',
240 - $blog_prefix . 'activitypub_icon',
603 + \register_post_meta(
604 + Outbox::POST_TYPE,
605 + '_activitypub_activity_actor',
241 606 array(
242 - 'type' => 'integer',
243 - 'description' => 'The attachment ID for user profile image.',
607 + 'type' => 'string',
244 608 'single' => true,
245 - 'default' => 0,
246 - 'sanitize_callback' => 'absint',
609 + 'show_in_rest' => true,
610 + 'sanitize_callback' => function ( $value ) {
611 + $schema = array(
612 + 'type' => 'string',
613 + 'enum' => array( 'application', 'blog', 'user' ),
614 + 'default' => 'user',
615 + );
616 +
617 + if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
618 + return $schema['default'];
619 + }
620 +
621 + return $value;
622 + },
247 623 )
248 624 );
249 625
250 - \register_meta(
251 - 'user',
252 - $blog_prefix . 'activitypub_header_image',
626 + \register_post_meta(
627 + Outbox::POST_TYPE,
628 + '_activitypub_outbox_offset',
253 629 array(
254 630 'type' => 'integer',
255 - 'description' => 'The attachment ID for the user header image.',
256 631 'single' => true,
632 + 'description' => 'Keeps track of the followers offset when processing outbox items.',
633 + 'sanitize_callback' => 'absint',
257 634 'default' => 0,
258 - 'sanitize_callback' => 'absint',
259 635 )
260 636 );
261 637
262 - \register_meta(
263 - 'user',
264 - $blog_prefix . 'activitypub_mailer_new_dm',
638 + \register_post_meta(
639 + Outbox::POST_TYPE,
640 + '_activitypub_object_id',
265 641 array(
266 - 'type' => 'integer',
267 - 'description' => 'Send a notification when someone sends this user a direct message.',
642 + 'type' => 'string',
268 643 'single' => true,
269 - 'sanitize_callback' => 'absint',
644 + 'description' => 'The ID (ActivityPub URI) of the object that the outbox item is about.',
645 + 'sanitize_callback' => 'sanitize_url',
270 646 )
271 647 );
272 - \add_filter( 'get_user_option_activitypub_mailer_new_dm', array( self::class, 'user_options_default' ) );
273 648
274 - \register_meta(
275 - 'user',
276 - $blog_prefix . 'activitypub_mailer_new_follower',
649 + \register_post_meta(
650 + Outbox::POST_TYPE,
651 + 'activitypub_content_visibility',
277 652 array(
278 - 'type' => 'integer',
279 - 'description' => 'Send a notification when someone starts to follow this user.',
653 + 'type' => 'string',
280 654 'single' => true,
281 - 'sanitize_callback' => 'absint',
655 + 'show_in_rest' => true,
656 + 'sanitize_callback' => function ( $value ) {
657 + $schema = array(
658 + 'type' => 'string',
659 + 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ),
660 + 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC,
661 + );
662 +
663 + if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
664 + return $schema['default'];
665 + }
666 +
667 + return $value;
668 + },
282 669 )
283 670 );
284 - \add_filter( 'get_user_option_activitypub_mailer_new_follower', array( self::class, 'user_options_default' ) );
285 671
672 + // Both User and Blog Extra Fields types have the same args.
673 + $args = array(
674 + 'labels' => array(
675 + 'name' => _x( 'Extra fields', 'post_type plural name', 'activitypub' ),
676 + 'singular_name' => _x( 'Extra field', 'post_type single name', 'activitypub' ),
677 + 'add_new' => __( 'Add new', 'activitypub' ),
678 + 'add_new_item' => __( 'Add new extra field', 'activitypub' ),
679 + 'new_item' => __( 'New extra field', 'activitypub' ),
680 + 'edit_item' => __( 'Edit extra field', 'activitypub' ),
681 + 'view_item' => __( 'View extra field', 'activitypub' ),
682 + 'all_items' => __( 'All extra fields', 'activitypub' ),
683 + ),
684 + 'public' => false,
685 + 'hierarchical' => false,
686 + 'query_var' => false,
687 + 'has_archive' => false,
688 + 'publicly_queryable' => false,
689 + 'show_in_menu' => false,
690 + 'delete_with_user' => true,
691 + 'can_export' => true,
692 + 'exclude_from_search' => true,
693 + 'show_in_rest' => true,
694 + 'map_meta_cap' => true,
695 + 'show_ui' => true,
696 + 'supports' => array( 'title', 'editor', 'page-attributes' ),
697 + );
698 +
699 + \register_post_type( Extra_Fields::USER_POST_TYPE, $args );
700 + \register_post_type( Extra_Fields::BLOG_POST_TYPE, $args );
701 +
702 + /**
703 + * Fires after ActivityPub custom post types have been registered.
704 + */
705 + \do_action( 'activitypub_after_register_post_type' );
706 + }
707 +
708 + /**
709 + * Add the 'activitypub' capability to users who can publish posts.
710 + *
711 + * @param int $user_id User ID.
712 + */
713 + public static function user_register( $user_id ) {
714 + if ( \user_can( $user_id, 'publish_posts' ) ) {
715 + $user = \get_user_by( 'id', $user_id );
716 + $user->add_cap( 'activitypub' );
717 + }
718 + }
719 +
720 + /**
721 + * Delete `activitypub_content_visibility` when updated to an empty value.
722 + *
723 + * @param int $meta_id ID of updated metadata entry.
724 + * @param int $object_id Post ID.
725 + * @param string $meta_key Metadata key.
726 + * @param mixed $meta_value Metadata value. This will be a PHP-serialized string representation of the value
727 + * if the value is an array, an object, or itself a PHP-serialized string.
728 + */
729 + public static function updated_postmeta( $meta_id, $object_id, $meta_key, $meta_value ) {
730 + if ( 'activitypub_content_visibility' === $meta_key && empty( $meta_value ) ) {
731 + \delete_post_meta( $object_id, 'activitypub_content_visibility' );
732 + }
733 + }
734 +
735 + /**
736 + * Register some Mastodon oEmbed providers.
737 + */
738 + public static function register_oembed_providers() {
739 + \wp_oembed_add_provider( '#https?://mastodon\.social/(@.+)/([0-9]+)#i', 'https://mastodon.social/api/oembed', true );
740 + \wp_oembed_add_provider( '#https?://mastodon\.online/(@.+)/([0-9]+)#i', 'https://mastodon.online/api/oembed', true );
741 + \wp_oembed_add_provider( '#https?://mastodon\.cloud/(@.+)/([0-9]+)#i', 'https://mastodon.cloud/api/oembed', true );
742 + \wp_oembed_add_provider( '#https?://mstdn\.social/(@.+)/([0-9]+)#i', 'https://mstdn.social/api/oembed', true );
743 + \wp_oembed_add_provider( '#https?://mastodon\.world/(@.+)/([0-9]+)#i', 'https://mastodon.world/api/oembed', true );
744 + \wp_oembed_add_provider( '#https?://mas\.to/(@.+)/([0-9]+)#i', 'https://mas.to/api/oembed', true );
745 + }
746 +
747 + /**
748 + * Register user meta.
749 + */
750 + public static function register_user_meta() {
751 + $blog_prefix = $GLOBALS['wpdb']->get_blog_prefix();
752 +
286 753 \register_meta(
287 754 'user',
288 - $blog_prefix . 'activitypub_mailer_new_mention',
755 + $blog_prefix . 'activitypub_also_known_as',
289 756 array(
290 - 'type' => 'integer',
291 - 'description' => 'Send a notification when someone mentions this user.',
757 + 'type' => 'array',
758 + 'description' => 'An array of URLs that the user is known by.',
292 759 'single' => true,
293 - 'sanitize_callback' => 'absint',
760 + 'default' => array(),
761 + 'sanitize_callback' => array( Sanitize::class, 'url_list' ),
294 762 )
295 763 );
296 - \add_filter( 'get_user_option_activitypub_mailer_new_mention', array( self::class, 'user_options_default' ) );
297 764
298 765 \register_meta(
299 766 'user',
300 - $blog_prefix . 'activitypub_mailer_annual_report',
767 + $blog_prefix . 'activitypub_old_host_data',
301 768 array(
302 - 'type' => 'integer',
303 - 'description' => 'Send the annual Fediverse Year in Review email.',
304 - 'single' => true,
305 - 'sanitize_callback' => 'absint',
769 + 'description' => 'Actor object for the user on the old host.',
770 + 'single' => true,
306 771 )
307 772 );
308 - \add_filter( 'get_user_option_activitypub_mailer_annual_report', array( self::class, 'user_options_default' ) );
309 773
310 774 \register_meta(
311 775 'user',
312 - $blog_prefix . 'activitypub_mailer_monthly_report',
776 + $blog_prefix . 'activitypub_moved_to',
313 777 array(
314 - 'type' => 'integer',
315 - 'description' => 'Send a monthly Fediverse stats report email.',
778 + 'type' => 'string',
779 + 'description' => 'The new URL of the user.',
316 780 'single' => true,
317 - 'sanitize_callback' => 'absint',
781 + 'sanitize_callback' => 'sanitize_url',
318 782 )
319 783 );
320 784
321 785 \register_meta(
322 786 'user',
323 - 'activitypub_show_welcome_tab',
787 + $blog_prefix . 'activitypub_description',
324 788 array(
325 - 'type' => 'integer',
326 - 'description' => 'Whether to show the welcome tab.',
789 + 'type' => 'string',
790 + 'description' => 'The user’s description.',
327 791 'single' => true,
328 - 'default' => 1,
329 - 'sanitize_callback' => 'absint',
792 + 'default' => '',
793 + 'sanitize_callback' => function ( $value ) {
794 + return wp_kses( $value, 'user_description' );
795 + },
330 796 )
331 797 );
332 798
333 799 \register_meta(
334 800 'user',
335 - 'activitypub_show_advanced_tab',
801 + $blog_prefix . 'activitypub_icon',
336 802 array(
337 803 'type' => 'integer',
338 - 'description' => 'Whether to show the advanced tab.',
804 + 'description' => 'The attachment ID for user’s profile image.',
339 805 'single' => true,
340 806 'default' => 0,
341 807 'sanitize_callback' => 'absint',
342 808 )
@@ -341,60 +807,41 @@
341 807 'sanitize_callback' => 'absint',
342 808 )
343 809 );
344 810
345 - // Moderation user meta.
346 811 \register_meta(
347 812 'user',
348 - 'activitypub_blocked_actors',
813 + $blog_prefix . 'activitypub_header_image',
349 814 array(
350 - 'type' => 'array',
351 - 'description' => 'User-specific blocked ActivityPub actors.',
815 + 'type' => 'integer',
816 + 'description' => 'The attachment ID for the user’s header image.',
352 817 'single' => true,
353 - 'default' => array(),
354 - 'sanitize_callback' => array( Sanitize::class, 'identifier_list' ),
818 + 'default' => 0,
819 + 'sanitize_callback' => 'absint',
355 820 )
356 821 );
357 822
358 823 \register_meta(
359 824 'user',
360 - 'activitypub_blocked_domains',
825 + 'activitypub_show_welcome_tab',
361 826 array(
362 - 'type' => 'array',
363 - 'description' => 'User-specific blocked ActivityPub domains.',
827 + 'type' => 'integer',
828 + 'description' => 'Whether to show the welcome tab.',
364 829 'single' => true,
365 - 'default' => array(),
366 - 'sanitize_callback' => static function ( $value ) {
367 - return \array_unique( \array_map( array( Sanitize::class, 'host_list' ), $value ) );
368 - },
830 + 'default' => 1,
831 + 'sanitize_callback' => 'absint',
369 832 )
370 833 );
371 834
372 835 \register_meta(
373 836 'user',
374 - 'activitypub_blocked_keywords',
837 + 'activitypub_show_advanced_tab',
375 838 array(
376 - 'type' => 'array',
377 - 'description' => 'User-specific blocked ActivityPub keywords.',
839 + 'type' => 'integer',
840 + 'description' => 'Whether to show the advanced tab.',
378 841 'single' => true,
379 - 'default' => array(),
380 - 'sanitize_callback' => static function ( $value ) {
381 - return \array_map( 'sanitize_text_field', $value );
382 - },
842 + 'default' => 0,
843 + 'sanitize_callback' => 'absint',
383 844 )
384 845 );
385 - }
386 -
387 - /**
388 - * Set default values for user options.
389 - *
390 - * @param bool|string $value Option value.
391 - * @return bool|string
392 - */
393 - public static function user_options_default( $value ) {
394 - if ( false === $value ) {
395 - return '1';
396 - }
397 -
398 - return $value;
399 846 }
400 847 }