PluginProbe
ActivityPub / 7.7.0
ActivityPub v7.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-router.php +18 -171 9.2.07.7.0 View file →
@@ -19,9 +19,8 @@
19 19 */
20 20 public static function init() {
21 21 \add_action( 'init', array( self::class, 'add_rewrite_rules' ), 11 );
22 22
23 - \add_action( 'send_headers', array( self::class, 'add_headers' ) );
24 23 \add_filter( 'template_include', array( self::class, 'render_activitypub_template' ), 99 );
25 24 \add_action( 'template_redirect', array( self::class, 'template_redirect' ) );
26 25 \add_filter( 'redirect_canonical', array( self::class, 'redirect_canonical' ), 10, 2 );
27 26 \add_filter( 'redirect_canonical', array( self::class, 'no_trailing_redirect' ), 10, 2 );
@@ -41,14 +40,8 @@
41 40 if ( ACTIVITYPUB_DISABLE_REWRITES ) {
42 41 return;
43 42 }
44 43
45 - \add_rewrite_rule(
46 - '^authorize_interaction/?$',
47 - 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions',
48 - 'top'
49 - );
50 -
51 44 if ( ! \class_exists( 'Webfinger' ) ) {
52 45 \add_rewrite_rule(
53 46 '^.well-known/webfinger',
54 47 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger',
@@ -63,17 +56,8 @@
63 56 'top'
64 57 );
65 58 }
66 59
67 - // Authorization Server Metadata (RFC 8414).
68 - \add_rewrite_rule(
69 - '^.well-known/oauth-authorization-server',
70 - 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/oauth/authorization-server-metadata',
71 - 'top'
72 - );
73 -
74 - // Must precede the generic @username rule, which would otherwise match "application" as an actor username.
75 - \add_rewrite_rule( '^@application\/?$', 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/application', 'top' );
76 60 \add_rewrite_rule( '^@([\w\-\.]+)\/?$', 'index.php?actor=$matches[1]', 'top' );
77 61 \add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
78 62 }
79 63
@@ -88,103 +72,36 @@
88 72 if ( \wp_is_serving_rest_request() || \wp_doing_ajax() ) {
89 73 return $template;
90 74 }
91 75
76 + self::add_headers();
77 +
92 78 if ( ! is_activitypub_request() || ! should_negotiate_content() ) {
93 - $is_outbox_item = \get_query_var( 'p' ) && Outbox::POST_TYPE === \get_post_type( \get_query_var( 'p' ) );
94 - $is_preflight = isset( $_SERVER['REQUEST_METHOD'] ) && 'OPTIONS' === $_SERVER['REQUEST_METHOD'];
95 -
96 - if ( $is_outbox_item && $is_preflight ) {
97 - /*
98 - * CORS preflight: override WordPress 404 so the browser
99 - * accepts the preflight response (must be 2xx).
100 - */
101 - \status_header( 200 );
102 - } elseif ( $is_outbox_item ) {
103 - // Return 406 for non-ActivityPub requests to outbox items since they only support ActivityPub requests.
79 + if ( \get_query_var( 'p' ) && Outbox::POST_TYPE === \get_post_type( \get_query_var( 'p' ) ) ) {
104 80 \set_query_var( 'is_404', true );
105 81 \status_header( 406 );
106 82 }
107 -
108 83 return $template;
109 84 }
110 85
111 - $activitypub_object = Query::get_instance()->get_activitypub_object();
112 - $queried_object = Query::get_instance()->get_queried_object();
113 -
114 - /*
115 - * Serve the Tombstone for a deleted object — but not while an authorized
116 - * user is previewing it. During an editor `?preview=true` request,
117 - * `is_post_publicly_queryable()` treats a draft or pending post as
118 - * queryable only for a user who can edit it, so the author can still use
119 - * the Fediverse Preview on a post they just soft-deleted (which is
120 - * otherwise already in the tombstone registry).
121 - *
122 - * The bypass is scoped to that preview request on purpose. A normal
123 - * ActivityPub fetch (no `preview`) of a tombstoned URL always gets the
124 - * Tombstone — even if the URL now resolves to a fresh public post because
125 - * its slug was reused — since remote servers were told that id is gone.
126 - * The legitimate restore path clears the registry entry itself, via
127 - * `Create::maybe_unbury()` when the re-publish Create is queued.
128 - */
129 - $is_authorized_preview = \get_query_var( 'preview' )
130 - && $queried_object instanceof \WP_Post
131 - && is_post_publicly_queryable( $queried_object );
132 -
133 - if (
134 - Tombstone::exists_local( Query::get_instance()->get_request_url() )
135 - && ! $is_authorized_preview
136 - ) {
137 - // Set 410 Gone for permanently deleted posts, 200 OK for soft-deleted.
138 - if ( ! $activitypub_object ) {
139 - \status_header( 410 );
140 - }
141 -
86 + if ( Tombstone::exists_local( Query::get_instance()->get_request_url() ) ) {
87 + \status_header( 410 );
142 88 return ACTIVITYPUB_PLUGIN_DIR . 'templates/tombstone-json.php';
143 89 }
144 90
145 - /*
146 - * Refuse to expose the content-negotiated representation of a post
147 - * that is no longer publicly queryable (non-public status, AP
148 - * visibility flipped, post-type support removed, etc.). The
149 - * lifecycle gate in `is_post_disabled()` intentionally lets such
150 - * posts through the federation pipeline so a Delete can fire, but
151 - * that escape hatch must not leak into front-end rendering during
152 - * the window between status change and Delete delivery.
153 - */
154 - if (
155 - $activitypub_object &&
156 - $queried_object instanceof \WP_Post &&
157 - 'ap_outbox' !== $queried_object->post_type &&
158 - ! is_post_publicly_queryable( $queried_object )
159 - ) {
160 - return $template;
161 - }
162 -
163 91 $activitypub_template = false;
92 + $activitypub_object = Query::get_instance()->get_activitypub_object();
164 93
165 94 if ( $activitypub_object ) {
166 95 if ( \get_query_var( 'preview' ) ) {
167 - \defined( 'ACTIVITYPUB_PREVIEW' ) || \define( 'ACTIVITYPUB_PREVIEW', true );
96 + \define( 'ACTIVITYPUB_PREVIEW', true );
168 97
169 - /*
170 - * A preview is only ever reached by someone allowed to see the unpublished post:
171 - * is_post_publicly_queryable() gates draft/pending/future on current_user_can('edit_post'),
172 - * and everyone else has already fallen through to the normal template above. The response
173 - * therefore varies by caller no matter the Authorized Fetch setting, so it must never be
174 - * stored by a shared cache and replayed to someone who cannot edit the post. Independent of
175 - * the Authorized Fetch block below, which only guards the signed-fetch path.
176 - */
177 - if ( ! \headers_sent() ) {
178 - \header( 'Cache-Control: private, no-store, max-age=0' );
179 - }
180 -
181 98 /**
182 99 * Filter the template used for the ActivityPub preview.
183 100 *
184 101 * @param string $activitypub_template Absolute path to the template file.
185 102 */
186 - $activitypub_template = \apply_filters( 'activitypub_preview_template', ACTIVITYPUB_PLUGIN_DIR . '/templates/post-preview.php' );
103 + $activitypub_template = apply_filters( 'activitypub_preview_template', ACTIVITYPUB_PLUGIN_DIR . '/templates/post-preview.php' );
187 104 } else {
188 105 $activitypub_template = ACTIVITYPUB_PLUGIN_DIR . 'templates/activitypub-json.php';
189 106 }
190 107 }
@@ -195,19 +112,8 @@
195 112 * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
196 113 * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
197 114 */
198 115 if ( $activitypub_template && use_authorized_fetch() ) {
199 - /*
200 - * Under Authorized Fetch the response depends on the caller's signature: an unsigned request
201 - * is refused, a signed one gets the document. A shared cache (page cache or CDN) keys the
202 - * activity+json variant on the representation, not the signature, so it must store neither
203 - * outcome and replay it to the wrong caller. Send this before verifying so it covers the 401
204 - * and the 200 alike; `max-age=0` is what page caches such as Surge key on to skip storing.
205 - */
206 - if ( ! \headers_sent() ) {
207 - \header( 'Cache-Control: private, no-store, max-age=0' );
208 - }
209 -
210 116 $verification = Signature::verify_http_signature( $_SERVER );
211 117 if ( \is_wp_error( $verification ) ) {
212 118 \status_header( 401 );
213 119
@@ -236,29 +142,14 @@
236 142 */
237 143 public static function add_headers() {
238 144 $id = Query::get_instance()->get_activitypub_object_id();
239 145
240 - /*
241 - * Send CORS headers for resolved ActivityPub objects and outbox
242 - * items. Outbox items need CORS even when the object ID doesn't
243 - * resolve, because browser preflight requests don't carry the
244 - * Authorization header needed to authenticate private items.
245 - */
246 - $post_id = \get_query_var( 'p' );
247 - $is_outbox_url = $post_id && Outbox::POST_TYPE === \get_post_type( $post_id );
248 -
249 - if ( ! \headers_sent() && ( $id || $is_outbox_url ) ) {
250 - \header( 'Access-Control-Allow-Origin: *' );
251 - \header( 'Access-Control-Allow-Methods: GET, OPTIONS' );
252 - \header( 'Access-Control-Allow-Headers: Accept, Authorization, Content-Type' );
253 - }
254 -
255 146 if ( ! $id ) {
256 147 return;
257 148 }
258 149
259 - if ( ! \headers_sent() ) {
260 - \header( 'Link: <' . \esc_url( $id ) . '>; title="ActivityPub (JSON)"; rel="alternate"; type="application/activity+json"', false );
150 + if ( ! headers_sent() ) {
151 + \header( 'Link: <' . esc_url( $id ) . '>; title="ActivityPub (JSON)"; rel="alternate"; type="application/activity+json"', false );
261 152
262 153 if ( \get_option( 'activitypub_vary_header', '1' ) ) {
263 154 // Send Vary header for Accept header.
264 155 \header( 'Vary: Accept', false );
@@ -264,12 +155,12 @@
264 155 \header( 'Vary: Accept', false );
265 156 }
266 157 }
267 158
268 - \add_action(
159 + add_action(
269 160 'wp_head',
270 - static function () use ( $id ) {
271 - echo PHP_EOL . '<link rel="alternate" title="ActivityPub (JSON)" type="application/activity+json" href="' . \esc_url( $id ) . '" />' . PHP_EOL;
161 + function () use ( $id ) {
162 + echo PHP_EOL . '<link rel="alternate" title="ActivityPub (JSON)" type="application/activity+json" href="' . esc_url( $id ) . '" />' . PHP_EOL;
272 163 }
273 164 );
274 165 }
275 166
@@ -281,9 +172,9 @@
281 172 *
282 173 * @return string $redirect_url The possibly-unslashed redirect URL.
283 174 */
284 175 public static function no_trailing_redirect( $redirect_url, $requested_url ) {
285 - if ( \get_query_var( 'actor' ) ) {
176 + if ( get_query_var( 'actor' ) ) {
286 177 return $requested_url;
287 178 }
288 179
289 180 return $redirect_url;
@@ -311,9 +202,9 @@
311 202 $query_params = \wp_parse_args( $query );
312 203 unset( $query_params['activitypub'] );
313 204 unset( $query_params['stamp'] );
314 205
315 - if ( 1 !== \count( $query_params ) ) {
206 + if ( 1 !== count( $query_params ) ) {
316 207 return $redirect_url;
317 208 }
318 209
319 210 if ( isset( $query_params['p'] ) ) {
@@ -351,22 +242,14 @@
351 242 if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
352 243 return;
353 244 }
354 245
355 - \wp_safe_redirect( \get_comment_link( $comment ) );
246 + \wp_safe_redirect( get_comment_link( $comment ) );
356 247 exit;
357 248 }
358 249
359 - /*
360 - * Skip the actor branch when this looks like an actor-scoped FEP-7aa9
361 - * stamp URL: numeric `actor` paired with a `stamp`. Those resolve to a
362 - * FeatureAuthorization via Activitypub\Query, not via the username
363 - * lookup which would 404 the numeric ID. Non-numeric actors fall
364 - * through to the regular Mastodon-style profile lookup.
365 - */
366 - $actor = \get_query_var( 'actor', null );
367 - $is_stamp_url = $actor && \get_query_var( 'stamp' ) && \ctype_digit( (string) $actor );
368 - if ( $actor && ! $is_stamp_url ) {
250 + $actor = \get_query_var( 'actor', null );
251 + if ( $actor ) {
369 252 $actor = Actors::get_by_username( $actor );
370 253 if ( ! $actor || \is_wp_error( $actor ) ) {
371 254 $wp_query->set_404();
372 255 return;
@@ -378,43 +261,8 @@
378 261
379 262 \wp_safe_redirect( $actor->get_url(), 301 );
380 263 exit;
381 264 }
382 -
383 - $term_id = \get_query_var( 'term_id', null );
384 - if ( $term_id ) {
385 - $term = \get_term( $term_id );
386 -
387 - // Load a 404-page if `term_id` is set but not valid.
388 - if ( ! $term || \is_wp_error( $term ) ) {
389 - $wp_query->set_404();
390 - return;
391 - }
392 -
393 - /**
394 - * Filters the taxonomies supported for term redirects.
395 - *
396 - * @since 7.8.3
397 - *
398 - * @param array $supported_taxonomies Array of taxonomy names. Default array( 'category', 'post_tag' ).
399 - */
400 - $supported_taxonomies = \apply_filters( 'activitypub_supported_taxonomies', array( 'category', 'post_tag' ) );
401 -
402 - if ( ! \in_array( $term->taxonomy, $supported_taxonomies, true ) ) {
403 - return;
404 - }
405 -
406 - // Don't redirect for ActivityPub requests.
407 - if ( is_activitypub_request() ) {
408 - return;
409 - }
410 -
411 - $term_link = \get_term_link( $term );
412 - if ( ! \is_wp_error( $term_link ) ) {
413 - \wp_safe_redirect( $term_link, 301 );
414 - exit;
415 - }
416 - }
417 265 }
418 266
419 267 /**
420 268 * Add the 'activitypub' query variable so WordPress won't mangle it.
@@ -431,9 +279,8 @@
431 279 $vars[] = 'stamp';
432 280 $vars[] = 'type';
433 281 $vars[] = 'c';
434 282 $vars[] = 'p';
435 - $vars[] = 'term_id';
436 283
437 284 return $vars;
438 285 }
439 286