PluginProbe
ActivityPub / 2.1.0
ActivityPub v2.1.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-activitypub.php

class-activitypub.php in ActivityPub 2.1.0, at includes/class-activitypub.php

441 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Activitypub;
3
4 use Exception;
5 use Activitypub\Signature;
6 use Activitypub\Collection\Users;
7 use Activitypub\Collection\Followers;
8
9 use function Activitypub\is_comment;
10 use function Activitypub\sanitize_url;
11 use function Activitypub\is_local_comment;
12 use function Activitypub\is_activitypub_request;
13 use function Activitypub\should_comment_be_federated;
14
15 /**
16 * ActivityPub Class
17 *
18 * @author Matthias Pfefferle
19 */
20 class Activitypub {
21 /**
22 * Initialize the class, registering WordPress hooks.
23 */
24 public static function init() {
25 \add_filter( 'template_include', array( self::class, 'render_json_template' ), 99 );
26 \add_action( 'template_redirect', array( self::class, 'template_redirect' ) );
27 \add_filter( 'query_vars', array( self::class, 'add_query_vars' ) );
28 \add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
29
30 // Add support for ActivityPub to custom post types
31 $post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post' ) ) : array();
32
33 foreach ( $post_types as $post_type ) {
34 \add_post_type_support( $post_type, 'activitypub' );
35 }
36
37 \add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 );
38 \add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 );
39
40 \add_action( 'init', array( self::class, 'add_rewrite_rules' ), 11 );
41
42 \add_action( 'after_setup_theme', array( self::class, 'theme_compat' ), 99 );
43
44 \add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) );
45
46 // register several post_types
47 self::register_post_types();
48 }
49
50 /**
51 * Activation Hook
52 *
53 * @return void
54 */
55 public static function activate() {
56 self::flush_rewrite_rules();
57 Scheduler::register_schedules();
58 }
59
60 /**
61 * Deactivation Hook
62 *
63 * @return void
64 */
65 public static function deactivate() {
66 self::flush_rewrite_rules();
67 Scheduler::deregister_schedules();
68 }
69
70 /**
71 * Uninstall Hook
72 *
73 * @return void
74 */
75 public static function uninstall() {
76 Scheduler::deregister_schedules();
77 }
78
79 /**
80 * Return a AS2 JSON version of an author, post or page.
81 *
82 * @param string $template The path to the template object.
83 *
84 * @return string The new path to the JSON template.
85 */
86 public static function render_json_template( $template ) {
87 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
88 return $template;
89 }
90
91 if ( ! is_activitypub_request() ) {
92 return $template;
93 }
94
95 $json_template = false;
96
97 // check if user can publish posts
98 if ( \is_author() && is_wp_error( Users::get_by_id( \get_the_author_meta( 'ID' ) ) ) ) {
99 return $template;
100 }
101
102 // check if blog-user is enabled
103 if ( \is_home() && is_wp_error( Users::get_by_id( Users::BLOG_USER_ID ) ) ) {
104 return $template;
105 }
106
107 if ( \is_author() ) {
108 $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/author-json.php';
109 } elseif ( is_comment() ) {
110 $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/comment-json.php';
111 } elseif ( \is_singular() ) {
112 $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php';
113 } elseif ( \is_home() ) {
114 $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php';
115 }
116
117 if ( ACTIVITYPUB_AUTHORIZED_FETCH ) {
118 $verification = Signature::verify_http_signature( $_SERVER );
119 if ( \is_wp_error( $verification ) ) {
120 // fallback as template_loader can't return http headers
121 return $template;
122 }
123 }
124
125 return $json_template;
126 }
127
128 /**
129 * Custom redirects for ActivityPub requests.
130 *
131 * @return void
132 */
133 public static function template_redirect() {
134 $comment_id = get_query_var( 'c', null );
135
136 // check if it seems to be a comment
137 if ( ! $comment_id ) {
138 return;
139 }
140
141 $comment = get_comment( $comment_id );
142
143 // load a 404 page if `c` is set but not valid
144 if ( ! $comment ) {
145 global $wp_query;
146 $wp_query->set_404();
147 return;
148 }
149
150 // stop if it's not an ActivityPub comment
151 if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
152 return;
153 }
154
155 wp_safe_redirect( get_comment_link( $comment ) );
156 exit;
157 }
158
159 /**
160 * Add the 'activitypub' query variable so WordPress won't mangle it.
161 */
162 public static function add_query_vars( $vars ) {
163 $vars[] = 'activitypub';
164 $vars[] = 'c';
165 $vars[] = 'p';
166
167 return $vars;
168 }
169
170 /**
171 * Replaces the default avatar.
172 *
173 * @param array $args Arguments passed to get_avatar_data(), after processing.
174 * @param int|string|object $id_or_email A user ID, email address, or comment object.
175 *
176 * @return array $args
177 */
178 public static function pre_get_avatar_data( $args, $id_or_email ) {
179 if (
180 ! $id_or_email instanceof \WP_Comment ||
181 ! isset( $id_or_email->comment_type ) ||
182 $id_or_email->user_id
183 ) {
184 return $args;
185 }
186
187 $allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) );
188 if (
189 ! empty( $id_or_email->comment_type ) &&
190 ! \in_array(
191 $id_or_email->comment_type,
192 (array) $allowed_comment_types,
193 true
194 )
195 ) {
196 $args['url'] = false;
197 /** This filter is documented in wp-includes/link-template.php */
198 return \apply_filters( 'get_avatar_data', $args, $id_or_email );
199 }
200
201 // Check if comment has an avatar.
202 $avatar = self::get_avatar_url( $id_or_email->comment_ID );
203
204 if ( $avatar ) {
205 if ( ! isset( $args['class'] ) || ! \is_array( $args['class'] ) ) {
206 $args['class'] = array( 'u-photo' );
207 } else {
208 $args['class'][] = 'u-photo';
209 $args['class'] = \array_unique( $args['class'] );
210 }
211 $args['url'] = $avatar;
212 $args['class'][] = 'avatar-activitypub';
213 }
214
215 return $args;
216 }
217
218 /**
219 * Function to retrieve Avatar URL if stored in meta.
220 *
221 * @param int|WP_Comment $comment
222 *
223 * @return string $url
224 */
225 public static function get_avatar_url( $comment ) {
226 if ( \is_numeric( $comment ) ) {
227 $comment = \get_comment( $comment );
228 }
229 return \get_comment_meta( $comment->comment_ID, 'avatar_url', true );
230 }
231
232 /**
233 * Store permalink in meta, to send delete Activity.
234 *
235 * @param string $post_id The Post ID.
236 *
237 * @return void
238 */
239 public static function trash_post( $post_id ) {
240 \add_post_meta(
241 $post_id,
242 'activitypub_canonical_url',
243 \get_permalink( $post_id ),
244 true
245 );
246 }
247
248 /**
249 * Delete permalink from meta
250 *
251 * @param string $post_id The Post ID
252 *
253 * @return void
254 */
255 public static function untrash_post( $post_id ) {
256 \delete_post_meta( $post_id, 'activitypub_canonical_url' );
257 }
258
259 /**
260 * Add rewrite rules
261 */
262 public static function add_rewrite_rules() {
263 // If another system needs to take precedence over the ActivityPub rewrite rules,
264 // they can define their own and will manually call the appropriate functions as required.
265 if ( ACTIVITYPUB_DISABLE_REWRITES ) {
266 return;
267 }
268
269 if ( ! \class_exists( 'Webfinger' ) ) {
270 \add_rewrite_rule(
271 '^.well-known/webfinger',
272 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger',
273 'top'
274 );
275 }
276
277 if ( ! \class_exists( 'Nodeinfo_Endpoint' ) && true === (bool) \get_option( 'blog_public', 1 ) ) {
278 \add_rewrite_rule(
279 '^.well-known/nodeinfo',
280 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo/discovery',
281 'top'
282 );
283 \add_rewrite_rule(
284 '^.well-known/x-nodeinfo2',
285 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo2',
286 'top'
287 );
288 }
289
290 \add_rewrite_rule(
291 '^@([\w\-\.]+)',
292 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/users/$matches[1]',
293 'top'
294 );
295
296 \add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
297 }
298
299 /**
300 * Flush rewrite rules;
301 */
302 public static function flush_rewrite_rules() {
303 self::add_rewrite_rules();
304 \flush_rewrite_rules();
305 }
306
307 /**
308 * Theme compatibility stuff
309 *
310 * @return void
311 */
312 public static function theme_compat() {
313 $site_icon = get_theme_support( 'custom-logo' );
314
315 if ( ! $site_icon ) {
316 // custom logo support
317 add_theme_support(
318 'custom-logo',
319 array(
320 'height' => 80,
321 'width' => 80,
322 )
323 );
324 }
325
326 $custom_header = get_theme_support( 'custom-header' );
327
328 if ( ! $custom_header ) {
329 // This theme supports a custom header
330 $custom_header_args = array(
331 'width' => 1250,
332 'height' => 600,
333 'header-text' => true,
334 );
335 add_theme_support( 'custom-header', $custom_header_args );
336 }
337 }
338
339 /**
340 * Display plugin upgrade notice to users
341 *
342 * @param array $data The plugin data
343 *
344 * @return void
345 */
346 public static function plugin_update_message( $data ) {
347 if ( ! isset( $data['upgrade_notice'] ) ) {
348 return;
349 }
350
351 printf(
352 '<div class="update-message">%s</div>',
353 wp_kses(
354 wpautop( $data['upgrade_notice '] ),
355 array(
356 'p' => array(),
357 'a' => array( 'href', 'title' ),
358 'strong' => array(),
359 'em' => array(),
360 )
361 )
362 );
363 }
364
365 /**
366 * Register the "Followers" Taxonomy
367 *
368 * @return void
369 */
370 private static function register_post_types() {
371 \register_post_type(
372 Followers::POST_TYPE,
373 array(
374 'labels' => array(
375 'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ),
376 'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ),
377 ),
378 'public' => false,
379 'hierarchical' => false,
380 'rewrite' => false,
381 'query_var' => false,
382 'delete_with_user' => false,
383 'can_export' => true,
384 'supports' => array(),
385 )
386 );
387
388 \register_post_meta(
389 Followers::POST_TYPE,
390 'activitypub_inbox',
391 array(
392 'type' => 'string',
393 'single' => true,
394 'sanitize_callback' => 'sanitize_url',
395 )
396 );
397
398 \register_post_meta(
399 Followers::POST_TYPE,
400 'activitypub_errors',
401 array(
402 'type' => 'string',
403 'single' => false,
404 'sanitize_callback' => function ( $value ) {
405 if ( ! is_string( $value ) ) {
406 throw new Exception( 'Error message is no valid string' );
407 }
408
409 return esc_sql( $value );
410 },
411 )
412 );
413
414 \register_post_meta(
415 Followers::POST_TYPE,
416 'activitypub_user_id',
417 array(
418 'type' => 'string',
419 'single' => false,
420 'sanitize_callback' => function ( $value ) {
421 return esc_sql( $value );
422 },
423 )
424 );
425
426 \register_post_meta(
427 Followers::POST_TYPE,
428 'activitypub_actor_json',
429 array(
430 'type' => 'string',
431 'single' => true,
432 'sanitize_callback' => function ( $value ) {
433 return sanitize_text_field( $value );
434 },
435 )
436 );
437
438 \do_action( 'activitypub_after_register_post_type' );
439 }
440 }
441