Skip to content

Commit 78dc8a4

Browse files
committed
Harden REST auth and adapter handling
Require authentication for all Microsub requests, tighten scope fallback by checking a configurable capability, and propagate WP_Error responses instead of treating them as success. This also improves adapter interoperability by passing user_id into timeline filters, clamping timeline limits, preserving aggregated search results, enforcing first-wins follow behavior, and updating Friends integration to use current feed/user discovery APIs.
1 parent 4b222e6 commit 78dc8a4

7 files changed

Lines changed: 168 additions & 36 deletions

File tree

includes/adapters/class-activitypub.php

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,10 @@ protected function get_object_types_for_user( $user_id ) {
153153
* @param array $result Current result with 'items' from other adapters.
154154
* @param string $channel Channel UID.
155155
* @param array $args Query arguments (after, before, limit).
156+
* @param int $user_id The user ID the timeline is requested for.
156157
* @return array Timeline data with 'items' and optional 'paging'.
157158
*/
158-
public function get_timeline( $result, $channel, $args ) {
159+
public function get_timeline( $result, $channel, $args, $user_id = 0 ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
159160
if ( ! self::is_available() ) {
160161
return $result;
161162
}
@@ -270,6 +271,11 @@ public function get_following( $result, $channel, $user_id ) {
270271
* @return array|null Feed data on success, null to pass to next adapter.
271272
*/
272273
public function follow( $result, $channel, $url, $user_id ) {
274+
// Respect the "first adapter that can handle the URL wins" contract.
275+
if ( null !== $result ) {
276+
return $result;
277+
}
278+
273279
if ( ! self::is_available() ) {
274280
return $result;
275281
}
@@ -313,14 +319,20 @@ public function unfollow( $result, $channel, $url, $user_id ) {
313319
return $result;
314320
}
315321

316-
// Use ActivityPub unfollow function.
322+
// Use ActivityPub unfollow function. Returns the Undo outbox ID, or 0 when
323+
// no matching Follow existed (i.e. this user was not following the actor).
317324
$unfollow_result = \Activitypub\unfollow( $actor->ID, $user_id );
318325

319-
if ( ! \is_wp_error( $unfollow_result ) ) {
320-
return true;
326+
if ( \is_wp_error( $unfollow_result ) ) {
327+
return false;
328+
}
329+
330+
// A falsy result (0) means nothing was actually unfollowed here; pass through.
331+
if ( ! $unfollow_result ) {
332+
return $result;
321333
}
322334

323-
return false;
335+
return true;
324336
}
325337

326338
/**

includes/adapters/class-friends.php

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public function can_handle_url( $url ) {
7676
}
7777

7878
// Try to discover feeds from the URL.
79-
$discovered = $friends->feed->discover_feeds( $url );
79+
$discovered = $friends->feed->discover_available_feeds( $url );
8080

8181
return ! \is_wp_error( $discovered ) && ! empty( $discovered );
8282
}
@@ -103,7 +103,7 @@ public function owns_feed( $url ) {
103103
* @return \Friends\User_Feed|null The feed or null if not found.
104104
*/
105105
protected function get_feed_by_url( $url ) {
106-
$friend_users = \Friends\User_Query::all_friends_subscriptions();
106+
$friend_users = \Friends\User_Query::all_associated_users();
107107

108108
foreach ( $friend_users->get_results() as $friend_user ) {
109109
if ( ! $friend_user instanceof \Friends\User ) {
@@ -309,9 +309,10 @@ public function delete_channel( $result, $uid, $user_id ) {
309309
* @param array $result Current result with 'items' from other adapters.
310310
* @param string $channel Channel UID.
311311
* @param array $args Query arguments (after, before, limit).
312+
* @param int $user_id The user ID the timeline is requested for.
312313
* @return array Timeline data with 'items' and optional 'paging'.
313314
*/
314-
public function get_timeline( $result, $channel, $args ) {
315+
public function get_timeline( $result, $channel, $args, $user_id = 0 ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
315316
if ( ! self::is_available() ) {
316317
return $result;
317318
}
@@ -466,7 +467,7 @@ public function get_following( $result, $channel, $user_id ) {
466467
protected function get_friend_users_for_channel( $channel ) {
467468
// For home, notifications, and format channels, return all friends.
468469
if ( 'home' === $channel || 'notifications' === $channel || str_starts_with( $channel, 'friends-' ) ) {
469-
$query = \Friends\User_Query::all_friends_subscriptions();
470+
$query = \Friends\User_Query::all_associated_users();
470471
return $query->get_results();
471472
}
472473

@@ -508,23 +509,80 @@ protected function get_friend_users_for_channel( $channel ) {
508509
* @return array|null Feed data on success, null to pass to next adapter.
509510
*/
510511
public function follow( $result, $channel, $url, $user_id ) {
512+
// Respect the "first adapter that can handle the URL wins" contract.
513+
if ( null !== $result ) {
514+
return $result;
515+
}
516+
511517
if ( ! self::is_available() ) {
512518
return $result;
513519
}
514520

515-
// Check if we can handle this URL.
516-
if ( ! $this->can_handle_url( $url ) ) {
521+
$friends = $this->get_friends();
522+
if ( ! $friends || ! isset( $friends->feed ) ) {
523+
return $result;
524+
}
525+
526+
// Discover subscribable feeds at the URL.
527+
$feeds = $friends->feed->discover_available_feeds( $url );
528+
529+
if ( \is_wp_error( $feeds ) || empty( $feeds ) ) {
517530
return $result;
518531
// Pass to next adapter.
519532
}
520533

521-
// Use Friends plugin to subscribe to the URL.
522-
$friend_user = \Friends\Subscription::subscribe( $url );
534+
// Derive a user login and display name from the discovered feeds.
535+
$user_login = \Friends\User::get_user_login_from_feeds( $feeds );
536+
$display_name = \Friends\User::get_display_name_from_feeds( $feeds );
537+
538+
if ( empty( $user_login ) ) {
539+
$user_login = \sanitize_user( (string) \wp_parse_url( $url, \PHP_URL_HOST ), true );
540+
}
541+
542+
if ( empty( $user_login ) ) {
543+
return $result;
544+
}
545+
546+
// Pull an avatar and description from the discovered feeds, if available.
547+
$avatar = null;
548+
$description = null;
549+
foreach ( $feeds as $feed_details ) {
550+
if ( ! $avatar && ! empty( $feed_details['avatar'] ) ) {
551+
$avatar = $feed_details['avatar'];
552+
}
553+
if ( ! $description && ! empty( $feed_details['description'] ) ) {
554+
$description = $feed_details['description'];
555+
}
556+
}
557+
558+
// Create (or fetch) the subscription user.
559+
$friend_user = \Friends\User::create( $user_login, 'subscription', $url, $display_name, $avatar, $description );
523560

524561
if ( \is_wp_error( $friend_user ) ) {
525562
return $result;
526563
}
527564

565+
// Subscribe to the discovered feeds (autoselected, or any supported parser).
566+
$subscribed = false;
567+
foreach ( $feeds as $feed_url => $feed_details ) {
568+
$autoselect = ! empty( $feed_details['autoselect'] );
569+
$unsupported = isset( $feed_details['parser'] ) && 'unsupported' === $feed_details['parser'];
570+
571+
if ( ! $autoselect && $unsupported ) {
572+
continue;
573+
}
574+
575+
$subscribed_feed = $friend_user->subscribe( $feed_url, $feed_details );
576+
577+
if ( ! \is_wp_error( $subscribed_feed ) ) {
578+
$subscribed = true;
579+
}
580+
}
581+
582+
if ( ! $subscribed ) {
583+
return $result;
584+
}
585+
528586
// If subscribing to a specific list channel, add to that list.
529587
if ( str_starts_with( $channel, 'tag-' ) ) {
530588
$slug = substr( $channel, 4 );
@@ -603,13 +661,15 @@ public function search( $result, $query, $user_id ) {
603661
}
604662

605663
// Use Friends feed discovery.
606-
$discovered = $friends->feed->discover_feeds( $query );
664+
$discovered = $friends->feed->discover_available_feeds( $query );
607665

608666
if ( \is_wp_error( $discovered ) || empty( $discovered ) ) {
609-
return array( 'results' => array() );
667+
// Nothing to add; preserve results from other adapters.
668+
return $result;
610669
}
611670

612-
$results = array();
671+
// Preserve results aggregated by earlier adapters.
672+
$results = ( \is_array( $result ) && isset( $result['results'] ) ) ? $result['results'] : array();
613673

614674
foreach ( $discovered as $feed_url => $feed_data ) {
615675
$item = array(
@@ -646,7 +706,7 @@ public function preview( $result, $url, $user_id ) {
646706
}
647707

648708
// Try to fetch and parse the feed.
649-
$discovered = $friends->feed->discover_feeds( $url );
709+
$discovered = $friends->feed->discover_available_feeds( $url );
650710

651711
if ( \is_wp_error( $discovered ) || empty( $discovered ) ) {
652712
return $result;

includes/adapters/class-wordpress.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ public function get_channels( $channels, $user_id ) { // phpcs:ignore Generic.Co
100100
* @param array $result Current result with 'items' from other adapters.
101101
* @param string $channel Channel UID.
102102
* @param array $args Query arguments (after, before, limit).
103+
* @param int $user_id The user ID the timeline is requested for.
103104
* @return array Timeline data with 'items' and optional 'paging'.
104105
*/
105-
public function get_timeline( $result, $channel, $args ) {
106+
public function get_timeline( $result, $channel, $args, $user_id = 0 ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
106107
$limit = isset( $args['limit'] ) ? \absint( $args['limit'] ) : 20;
107108

108109
// Local blog posts.
@@ -208,7 +209,9 @@ public function search( $result, $query, $user_id ) { // phpcs:ignore Generic.Co
208209
return $result;
209210
}
210211

211-
$results = array();
212+
// Preserve results aggregated by earlier adapters.
213+
$results = ( \is_array( $result ) && isset( $result['results'] ) ) ? $result['results'] : array();
214+
212215
foreach ( $posts as $post ) {
213216
$results[] = array(
214217
'type' => 'feed',

includes/class-adapter.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function register() {
6868
\add_filter( 'microsub_order_channels', array( $this, 'order_channels' ), $this->priority, 3 );
6969

7070
// Timeline operations.
71-
\add_filter( 'microsub_get_timeline', array( $this, 'get_timeline' ), $this->priority, 3 );
71+
\add_filter( 'microsub_get_timeline', array( $this, 'get_timeline' ), $this->priority, 4 );
7272
\add_filter( 'microsub_timeline_mark_read', array( $this, 'timeline_mark_read' ), $this->priority, 4 );
7373
\add_filter( 'microsub_timeline_mark_unread', array( $this, 'timeline_mark_unread' ), $this->priority, 4 );
7474
\add_filter( 'microsub_timeline_remove', array( $this, 'timeline_remove' ), $this->priority, 4 );
@@ -192,6 +192,11 @@ abstract public function get_channels( $channels, $user_id );
192192
* Results from multiple adapters are merged automatically.
193193
* Return only your adapter's items.
194194
*
195+
* Implementations may accept an optional fourth `$user_id` argument (the user
196+
* the timeline is requested for); it is passed by the `microsub_get_timeline`
197+
* filter. It is kept out of the abstract signature to preserve backwards
198+
* compatibility with existing three-parameter adapters.
199+
*
195200
* @param array $result Current result with 'items' from other adapters.
196201
* @param string $channel Channel UID.
197202
* @param array $args Query arguments.

includes/class-autoloader.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class Autoloader {
5151
public function __construct( $prefix, $path ) {
5252
$this->prefix = $prefix;
5353
$this->prefix_length = \strlen( $prefix );
54-
$this->path = \rtrim( $path . '/' );
54+
$this->path = \rtrim( $path, '/' ) . '/';
5555
}
5656

5757
/**

includes/class-microsub.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ public function init() {
6868
}
6969

7070
$this->register_hooks();
71-
$this->register_adapters();
7271

7372
$this->initialized = true;
7473
}
@@ -145,7 +144,7 @@ public function html_header() {
145144
*/
146145
public function http_header() {
147146
$endpoint = $this->get_endpoint();
148-
\header( \sprintf( 'Link: <%s>; rel="microsub"', \esc_url( $endpoint ) ), false );
147+
\header( \sprintf( 'Link: <%s>; rel="microsub"', \esc_url_raw( $endpoint ) ), false );
149148
}
150149

151150
/**

0 commit comments

Comments
 (0)