Skip to content

Commit 0132fca

Browse files
authored
Merge branch 'trunk' into fix/attachment-filename-query-params
2 parents 7ff2eb1 + b2495e1 commit 0132fca

File tree

9 files changed

+273
-12
lines changed

9 files changed

+273
-12
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fixed
3+
4+
Undefined array key warning in Scheduler::async_batch when called without arguments.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fixed
3+
4+
Preserve original ActivityPub activity timestamps when creating posts and comments instead of using current time.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fixed
3+
4+
Improved handling of unusual activity data to avoid errors when activities contain unexpected formats.

includes/class-scheduler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ public static function async_batch() {
472472

473473
self::lock( $key );
474474

475-
if ( \is_callable( $args[0] ) ) {
475+
if ( \is_callable( $args[0] ?? null ) ) {
476476
$callback = \array_shift( $args ); // Remove $callback from arguments.
477477
}
478478
$next = \call_user_func_array( $callback, $args );

includes/collection/class-inbox.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,12 @@ public static function add( $activity, $recipients ) {
139139
/**
140140
* Get the title of an activity recursively.
141141
*
142-
* @param Activity|Base_Object $activity_object The activity object.
142+
* @param Activity|Base_Object|array $activity_object The activity object.
143143
*
144144
* @return string The title.
145145
*/
146146
private static function get_object_title( $activity_object ) {
147-
if ( ! $activity_object ) {
147+
if ( ! $activity_object || is_array( $activity_object ) ) {
148148
return '';
149149
}
150150

includes/collection/class-interactions.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,16 +343,17 @@ public static function activity_to_comment( $activity ) {
343343
$webfinger = str_replace( 'acct:', '', $webfinger );
344344
}
345345

346-
$date = $activity['object']['published'] ?? 'now';
346+
$published = $activity['object']['published'] ?? $activity['published'] ?? 'now';
347+
$gm_date = \gmdate( 'Y-m-d H:i:s', \strtotime( $published ) );
347348

348349
$comment_data = array(
349350
'comment_author' => $comment_author ?? __( 'Anonymous', 'activitypub' ),
350351
'comment_author_url' => \esc_url_raw( $url ),
351352
'comment_content' => $comment_content,
352353
'comment_type' => 'comment',
353354
'comment_author_email' => $webfinger,
354-
'comment_date' => \get_date_from_gmt( \gmdate( 'Y-m-d H:i:s', \strtotime( $date ) ) ),
355-
'comment_date_gmt' => \gmdate( 'Y-m-d H:i:s', \strtotime( $date ) ),
355+
'comment_date' => \get_date_from_gmt( $gm_date ),
356+
'comment_date_gmt' => $gm_date,
356357
'comment_meta' => array(
357358
'source_id' => \esc_url_raw( object_to_uri( $activity['object'] ) ),
358359
'protocol' => 'activitypub',

includes/collection/class-posts.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Activitypub\Attachments;
1111
use Activitypub\Sanitize;
1212

13+
use function Activitypub\generate_post_summary;
1314
use function Activitypub\object_to_uri;
1415

1516
/**
@@ -187,13 +188,17 @@ private static function activity_to_post( $activity ) {
187188
return new \WP_Error( 'invalid_activity', __( 'Invalid activity format', 'activitypub' ) );
188189
}
189190

191+
$gm_date = \gmdate( 'Y-m-d H:i:s', \strtotime( $activity['published'] ?? 'now' ) );
192+
190193
return array(
191-
'post_title' => isset( $activity['name'] ) ? \wp_strip_all_tags( $activity['name'] ) : '',
192-
'post_content' => isset( $activity['content'] ) ? Sanitize::content( $activity['content'] ) : '',
193-
'post_excerpt' => isset( $activity['summary'] ) ? \wp_strip_all_tags( $activity['summary'] ) : '',
194-
'post_status' => 'publish',
195-
'post_type' => self::POST_TYPE,
196-
'guid' => isset( $activity['id'] ) ? \esc_url_raw( $activity['id'] ) : '',
194+
'post_title' => isset( $activity['name'] ) ? \wp_strip_all_tags( $activity['name'] ) : '',
195+
'post_content' => isset( $activity['content'] ) ? Sanitize::content( $activity['content'] ) : '',
196+
'post_excerpt' => isset( $activity['summary'] ) ? \wp_strip_all_tags( $activity['summary'] ) : generate_post_summary( $activity['content'] ?? '' ),
197+
'post_status' => 'publish',
198+
'post_type' => self::POST_TYPE,
199+
'post_date_gmt' => $gm_date,
200+
'post_date' => \get_date_from_gmt( $gm_date ),
201+
'guid' => isset( $activity['id'] ) ? \esc_url_raw( $activity['id'] ) : '',
197202
);
198203
}
199204

tests/phpunit/tests/includes/collection/class-test-inbox.php

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,195 @@ public function test_deduplicate_only_matches_specific_guid() {
858858
$this->assertEquals( 2, $count_second, 'Should still have two posts with second-activity GUID (not deduplicated)' );
859859
}
860860

861+
/**
862+
* Test adding activity when object is an array.
863+
*
864+
* This test verifies that the inbox can handle activities where get_object()
865+
* returns an array instead of an object, which can happen with certain
866+
* ActivityPub implementations.
867+
*
868+
* @covers ::add
869+
*/
870+
public function test_add_activity_with_array_object() {
871+
// Create an activity with an array as the object.
872+
$activity = new Activity();
873+
$activity->set_id( 'https://remote.example.com/activities/array-object' );
874+
$activity->set_type( 'Create' );
875+
$activity->set_actor( 'https://remote.example.com/users/testuser' );
876+
877+
// Set object as an array (this can happen with certain implementations).
878+
$activity->set_object(
879+
array(
880+
'id' => 'https://remote.example.com/objects/array-test',
881+
'type' => 'Note',
882+
'content' => 'Test content',
883+
)
884+
);
885+
886+
$user_id = 1;
887+
888+
// Add activity to inbox - should not throw an error.
889+
$inbox_id = Inbox::add( $activity, $user_id );
890+
891+
$this->assertIsInt( $inbox_id );
892+
$this->assertGreaterThan( 0, $inbox_id );
893+
894+
// Verify the post was created with empty title (since array doesn't have get_name()).
895+
$post = \get_post( $inbox_id );
896+
$this->assertInstanceOf( 'WP_Post', $post );
897+
$this->assertEquals( Inbox::POST_TYPE, $post->post_type );
898+
899+
// Post title should be "[Create] " (activity type with no object title).
900+
$this->assertStringStartsWith( '[Create]', $post->post_title );
901+
}
902+
903+
/**
904+
* Test adding Flag activity with array of URLs as object.
905+
*
906+
* This test verifies that the inbox can handle Flag activities (used for
907+
* reporting content) where the object is an array of URLs, which is a
908+
* real-world scenario from Mastodon moderation reports.
909+
*
910+
* @covers ::add
911+
*/
912+
public function test_add_flag_activity_with_url_array() {
913+
// Create a Flag activity similar to moderation reports.
914+
$activity = new Activity();
915+
$activity->set_id( 'https://remote.example.com/activities/flag-report' );
916+
$activity->set_type( 'Flag' );
917+
$activity->set_actor( 'https://remote.example.com/users/reporter' );
918+
$activity->set_content( '' ); // Flag activities often have empty content.
919+
920+
// Set object as an array of URLs (actor being reported + content being reported).
921+
$activity->set_object(
922+
array(
923+
'https://example.org/users/reported-user',
924+
'https://example.org/posts/12345',
925+
)
926+
);
927+
928+
$user_id = 1;
929+
930+
// Add activity to inbox - should not throw an error despite array object.
931+
$inbox_id = Inbox::add( $activity, $user_id );
932+
933+
$this->assertIsInt( $inbox_id );
934+
$this->assertGreaterThan( 0, $inbox_id );
935+
936+
// Verify the post was created.
937+
$post = \get_post( $inbox_id );
938+
$this->assertInstanceOf( 'WP_Post', $post );
939+
$this->assertEquals( Inbox::POST_TYPE, $post->post_type );
940+
941+
// Post title should be "[Flag] " (activity type with no object title from array).
942+
$this->assertStringStartsWith( '[Flag]', $post->post_title );
943+
}
944+
945+
/**
946+
* Test adding activity when object is null.
947+
*
948+
* @covers ::add
949+
*/
950+
public function test_add_activity_with_null_object() {
951+
$activity = new Activity();
952+
$activity->set_id( 'https://remote.example.com/activities/null-object' );
953+
$activity->set_type( 'Delete' );
954+
$activity->set_actor( 'https://remote.example.com/users/testuser' );
955+
// Don't set any object - it will be null.
956+
957+
$inbox_id = Inbox::add( $activity, 1 );
958+
959+
$this->assertIsInt( $inbox_id );
960+
$this->assertGreaterThan( 0, $inbox_id );
961+
962+
// Post title should be "[Delete] " with no object title.
963+
$post = \get_post( $inbox_id );
964+
$this->assertStringStartsWith( '[Delete]', $post->post_title );
965+
}
966+
967+
/**
968+
* Test adding activity when object is a string URL.
969+
*
970+
* @covers ::add
971+
*/
972+
public function test_add_activity_with_string_object() {
973+
// Create a post to reference.
974+
$post_id = self::factory()->post->create(
975+
array(
976+
'post_title' => 'Referenced Post',
977+
'post_status' => 'publish',
978+
)
979+
);
980+
$post_url = \get_permalink( $post_id );
981+
982+
$activity = new Activity();
983+
$activity->set_id( 'https://remote.example.com/activities/string-object' );
984+
$activity->set_type( 'Like' );
985+
$activity->set_actor( 'https://remote.example.com/users/testuser' );
986+
$activity->set_object( $post_url ); // String URL.
987+
988+
$inbox_id = Inbox::add( $activity, 1 );
989+
990+
$this->assertIsInt( $inbox_id );
991+
992+
// Post title should include the referenced post title.
993+
$post = \get_post( $inbox_id );
994+
$this->assertStringContainsString( 'Referenced Post', $post->post_title );
995+
}
996+
997+
/**
998+
* Test adding activity with object that has name property.
999+
*
1000+
* @covers ::add
1001+
*/
1002+
public function test_add_activity_with_object_name() {
1003+
$activity = new Activity();
1004+
$activity->set_id( 'https://remote.example.com/activities/object-name' );
1005+
$activity->set_type( 'Create' );
1006+
$activity->set_actor( 'https://remote.example.com/users/testuser' );
1007+
1008+
$object = new Base_Object();
1009+
$object->set_id( 'https://remote.example.com/objects/named' );
1010+
$object->set_type( 'Note' );
1011+
$object->set_name( 'My Note Title' );
1012+
$object->set_content( 'This is the content' );
1013+
$activity->set_object( $object );
1014+
1015+
$inbox_id = Inbox::add( $activity, 1 );
1016+
1017+
$this->assertIsInt( $inbox_id );
1018+
1019+
// Post title should include the object name.
1020+
$post = \get_post( $inbox_id );
1021+
$this->assertStringContainsString( 'My Note Title', $post->post_title );
1022+
}
1023+
1024+
/**
1025+
* Test adding activity with object that has content but no name.
1026+
*
1027+
* @covers ::add
1028+
*/
1029+
public function test_add_activity_with_object_content_no_name() {
1030+
$activity = new Activity();
1031+
$activity->set_id( 'https://remote.example.com/activities/object-content' );
1032+
$activity->set_type( 'Create' );
1033+
$activity->set_actor( 'https://remote.example.com/users/testuser' );
1034+
1035+
$object = new Base_Object();
1036+
$object->set_id( 'https://remote.example.com/objects/content-only' );
1037+
$object->set_type( 'Note' );
1038+
$object->set_content( 'This is the content without a name' );
1039+
$activity->set_object( $object );
1040+
1041+
$inbox_id = Inbox::add( $activity, 1 );
1042+
1043+
$this->assertIsInt( $inbox_id );
1044+
1045+
// Post title should include part of the content (since no name).
1046+
$post = \get_post( $inbox_id );
1047+
$this->assertStringContainsString( 'This is the content', $post->post_title );
1048+
}
1049+
8611050
/**
8621051
* Test adding Like activity with trailing slash in object URL.
8631052
*

tests/phpunit/tests/includes/collection/class-test-posts.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,60 @@ public function test_activity_to_post_minimal() {
325325
$this->assertEquals( 'publish', $result['post_status'] );
326326
}
327327

328+
/**
329+
* Test that published timestamp is preserved when creating posts.
330+
*
331+
* @covers ::activity_to_post
332+
* @covers ::add
333+
*/
334+
public function test_preserves_published_timestamp() {
335+
$activity = array(
336+
'object' => array(
337+
'id' => 'https://example.com/objects/timestamp-test',
338+
'type' => 'Note',
339+
'name' => 'Timestamp Test',
340+
'content' => '<p>Test content</p>',
341+
'attributedTo' => 'https://example.com/users/testuser',
342+
'published' => '2023-06-15T14:30:00Z',
343+
),
344+
);
345+
346+
$result = Posts::add( $activity, 1 );
347+
348+
$this->assertInstanceOf( '\WP_Post', $result );
349+
$this->assertEquals( '2023-06-15 14:30:00', $result->post_date_gmt );
350+
$this->assertEquals( get_date_from_gmt( '2023-06-15 14:30:00' ), $result->post_date );
351+
}
352+
353+
/**
354+
* Test that activity_to_post handles missing content gracefully.
355+
*
356+
* @covers ::activity_to_post
357+
*/
358+
public function test_activity_to_post_missing_content() {
359+
$activity = array(
360+
'type' => 'Note',
361+
'name' => 'Title Only',
362+
'summary' => 'Summary text',
363+
);
364+
365+
// Use reflection to access the private method.
366+
$reflection = new \ReflectionClass( Posts::class );
367+
$method = $reflection->getMethod( 'activity_to_post' );
368+
$method->setAccessible( true );
369+
370+
try {
371+
$result = $method->invoke( null, $activity );
372+
} catch ( \Exception $exception ) {
373+
$result = $exception;
374+
}
375+
376+
$this->assertIsArray( $result );
377+
$this->assertEquals( 'Title Only', $result['post_title'] );
378+
$this->assertEquals( '', $result['post_content'] );
379+
$this->assertEquals( 'Summary text', $result['post_excerpt'] );
380+
}
381+
328382
/**
329383
* Test adding an object with multiple recipients.
330384
*

0 commit comments

Comments
 (0)