Skip to content

Commit 4313bc9

Browse files
authored
Follow[ers|ing]: Avoid errors when getting avatar url (#2088)
1 parent 289c6c9 commit 4313bc9

File tree

6 files changed

+271
-3
lines changed

6 files changed

+271
-3
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Add comprehensive unit tests for Followers and Following table classes with proper ActivityPub icon object handling.

includes/functions.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,8 @@ function object_to_uri( $data ) {
730730
// Return part of Object that makes most sense.
731731
switch ( $type ) {
732732
case 'Image':
733-
$data = $data['url'];
733+
// See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image.
734+
$data = object_to_uri( $data['url'] );
734735
break;
735736
case 'Link':
736737
$data = $data['href'];

includes/wp-admin/table/class-followers.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ public function prepare_items() {
223223

224224
$this->items[] = array(
225225
'id' => $follower->ID,
226-
'icon' => $actor->get_icon()['url'] ?? '',
226+
'icon' => object_to_uri( $actor->get_icon() ?? '' ),
227227
'post_title' => $actor->get_name() ?? $actor->get_preferred_username(),
228228
'username' => $actor->get_preferred_username(),
229229
'url' => $url,

includes/wp-admin/table/class-following.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ public function prepare_items() {
240240

241241
$this->items[] = array(
242242
'id' => $following->ID,
243-
'icon' => $actor->get_icon()['url'] ?? '',
243+
'icon' => object_to_uri( $actor->get_icon() ?? '' ),
244244
'post_title' => $actor->get_name() ?? $actor->get_preferred_username(),
245245
'username' => $actor->get_preferred_username(),
246246
'url' => $url,
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
/**
3+
* Test file for Followers Table.
4+
*
5+
* @package Activitypub
6+
*/
7+
8+
namespace Activitypub\Tests\WP_Admin\Table;
9+
10+
use Activitypub\WP_Admin\Table\Followers;
11+
use Activitypub\Collection\Followers as Follower_Collection;
12+
13+
/**
14+
* Test class for Followers Table.
15+
*
16+
* @coversDefaultClass \Activitypub\WP_Admin\Table\Followers
17+
*/
18+
class Test_Followers extends \WP_UnitTestCase {
19+
20+
/**
21+
* Followers table instance.
22+
*
23+
* @var Followers
24+
*/
25+
private $followers_table;
26+
27+
/**
28+
* Set up before each test.
29+
*/
30+
public function set_up() {
31+
parent::set_up();
32+
33+
// Set up global screen mock.
34+
set_current_screen( 'users_page_activitypub-followers-list' );
35+
36+
// Set current user.
37+
wp_set_current_user( 1 );
38+
39+
// Create followers table instance.
40+
$this->followers_table = new Followers();
41+
}
42+
43+
/**
44+
* Test column_username with actor having icon object.
45+
*
46+
* @covers ::column_username
47+
* @covers ::prepare_items
48+
*/
49+
public function test_column_username_with_icon_object() {
50+
// Mock remote metadata for the actor with icon object.
51+
$actor_url = 'https://example.com/users/testuser';
52+
$actor_data = array(
53+
'name' => 'Test User',
54+
'icon' => array(
55+
'type' => 'Image',
56+
'url' => 'https://secure.gravatar.com/avatar/example?s=120&d=mm&r=g',
57+
),
58+
'url' => $actor_url,
59+
'id' => 'https://example.com/users/testuser',
60+
'preferredUsername' => 'testuser',
61+
'inbox' => 'https://example.com/users/testuser/inbox',
62+
);
63+
64+
// Mock the remote metadata call using the correct filter.
65+
add_filter(
66+
'pre_get_remote_metadata_by_actor',
67+
function ( $value, $actor ) use ( $actor_url, $actor_data ) {
68+
if ( $actor === $actor_url ) {
69+
return $actor_data;
70+
}
71+
return $value;
72+
},
73+
10,
74+
2
75+
);
76+
77+
// Add the follower.
78+
Follower_Collection::add_follower( get_current_user_id(), $actor_url );
79+
80+
// Use the real prepare_items() method.
81+
$this->followers_table->prepare_items();
82+
83+
// Verify we have items.
84+
$this->assertNotEmpty( $this->followers_table->items );
85+
86+
// Get the first item and test column_username.
87+
$item = $this->followers_table->items[0];
88+
$result = $this->followers_table->column_username( $item );
89+
90+
// Verify the icon URL was extracted from the object by object_to_uri() and properly rendered.
91+
$this->assertStringContainsString( 'src="https://secure.gravatar.com/avatar/example?s=120&#038;d=mm&#038;r=g"', $result );
92+
93+
// Verify that the icon was processed correctly: from object to URL.
94+
$this->assertEquals( 'https://secure.gravatar.com/avatar/example?s=120&d=mm&r=g', $item['icon'] );
95+
96+
// Clean up.
97+
\remove_all_filters( 'pre_get_remote_metadata_by_actor' );
98+
}
99+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
/**
3+
* Test file for Following Table.
4+
*
5+
* @package Activitypub
6+
*/
7+
8+
namespace Activitypub\Tests\WP_Admin\Table;
9+
10+
use Activitypub\Collection\Actors;
11+
use Activitypub\Collection\Following as Following_Collection;
12+
use Activitypub\WP_Admin\Table\Following;
13+
14+
/**
15+
* Test class for Following Table.
16+
*
17+
* @coversDefaultClass \Activitypub\WP_Admin\Table\Following
18+
*/
19+
class Test_Following extends \WP_UnitTestCase {
20+
21+
/**
22+
* Following table instance.
23+
*
24+
* @var Following
25+
*/
26+
private $following_table;
27+
28+
/**
29+
* Set up before each test.
30+
*/
31+
public function set_up() {
32+
parent::set_up();
33+
34+
// Set up global screen mock.
35+
set_current_screen( 'users_page_activitypub-following-list' );
36+
37+
// Set current user.
38+
wp_set_current_user( 1 );
39+
40+
// Create following table instance.
41+
$this->following_table = new Following();
42+
}
43+
44+
/**
45+
* Test column_username with actor having icon object using real prepare_items().
46+
*
47+
* This test uses Following::follow() to create a real following and uses
48+
* the actual prepare_items() method to test the complete data flow from
49+
* ActivityPub actor with icon object to the final column output.
50+
*
51+
* @covers ::column_username
52+
* @covers ::prepare_items
53+
*/
54+
public function test_column_username_with_icon_object() {
55+
// Mock remote metadata for the actor with icon object.
56+
$actor_url = 'https://example.com/users/testuser';
57+
$actor_data = array(
58+
'name' => 'Test User',
59+
'icon' => array(
60+
'type' => 'Image',
61+
'url' => 'https://secure.gravatar.com/avatar/example?s=120&d=mm&r=g',
62+
),
63+
'url' => $actor_url,
64+
'id' => 'https://example.com/users/testuser',
65+
'preferredUsername' => 'testuser',
66+
'inbox' => 'https://example.com/users/testuser/inbox',
67+
);
68+
69+
// Mock the remote metadata call using the correct filter.
70+
add_filter(
71+
'pre_get_remote_metadata_by_actor',
72+
function ( $value, $actor ) use ( $actor_url, $actor_data ) {
73+
if ( $actor === $actor_url ) {
74+
return $actor_data;
75+
}
76+
return $value;
77+
},
78+
10,
79+
2
80+
);
81+
82+
// Add the actor first, then follow them.
83+
$actor_post_id = Actors::upsert( $actor_data );
84+
85+
// Follow the actor using the proper method.
86+
Following_Collection::follow( $actor_post_id, get_current_user_id() );
87+
88+
// Use the real prepare_items() method.
89+
$this->following_table->prepare_items();
90+
91+
// Verify we have items.
92+
$this->assertNotEmpty( $this->following_table->items );
93+
94+
// Get the first item and test column_username.
95+
$item = $this->following_table->items[0];
96+
$result = $this->following_table->column_username( $item );
97+
98+
// Verify the icon URL was extracted from the object by object_to_uri() and properly rendered.
99+
$this->assertStringContainsString( 'src="https://secure.gravatar.com/avatar/example?s=120&#038;d=mm&#038;r=g"', $result );
100+
101+
// Verify that the icon was processed correctly: from object to URL.
102+
$this->assertEquals( 'https://secure.gravatar.com/avatar/example?s=120&d=mm&r=g', $item['icon'] );
103+
104+
// Clean up.
105+
\remove_all_filters( 'pre_get_remote_metadata_by_actor' );
106+
wp_delete_post( $actor_post_id, true );
107+
}
108+
109+
/**
110+
* Test prepare_items with actor having icon array of URLs.
111+
*
112+
* This test verifies that when an icon field contains an array of URLs,
113+
* the object_to_uri() function correctly extracts the first URL from the array.
114+
*
115+
* @covers ::prepare_items
116+
*/
117+
public function test_prepare_items_with_icon_array_of_urls() {
118+
// Mock remote metadata for the actor with icon as direct array of URLs.
119+
$actor_url = 'https://example.com/users/arrayuser';
120+
$actor_data = array(
121+
'name' => 'Array User',
122+
'icon' => array(
123+
'url' => array(
124+
'https://example.com/storage/profile.webp',
125+
),
126+
'type' => 'Image',
127+
'mediaType' => 'image/webp',
128+
),
129+
'url' => $actor_url,
130+
'id' => 'https://example.com/users/arrayuser',
131+
'preferredUsername' => 'arrayuser',
132+
'inbox' => 'https://example.com/users/arrayuser/inbox',
133+
);
134+
135+
// Mock the remote metadata call using the correct filter.
136+
add_filter(
137+
'pre_get_remote_metadata_by_actor',
138+
function ( $value, $actor ) use ( $actor_url, $actor_data ) {
139+
if ( $actor === $actor_url ) {
140+
return $actor_data;
141+
}
142+
return $value;
143+
},
144+
10,
145+
2
146+
);
147+
148+
// Add the actor first, then follow them.
149+
$actor_post_id = Actors::upsert( $actor_data );
150+
151+
// Follow the actor using the proper method.
152+
Following_Collection::follow( $actor_post_id, get_current_user_id() );
153+
154+
// Use the real prepare_items() method.
155+
$this->following_table->prepare_items();
156+
157+
// Verify that the icon array was processed correctly: from array to first URL.
158+
$this->assertEquals( 'https://example.com/storage/profile.webp', $this->following_table->items[0]['icon'] );
159+
160+
// Clean up.
161+
\remove_all_filters( 'pre_get_remote_metadata_by_actor' );
162+
wp_delete_post( $actor_post_id, true );
163+
}
164+
}

0 commit comments

Comments
 (0)