Skip to content

Commit e202205

Browse files
authored
Refactor Mastodon import to use arrays, add tests (#2412)
1 parent 07ab03c commit e202205

File tree

5 files changed

+1023
-32
lines changed

5 files changed

+1023
-32
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+
Refactored Mastodon import handling to use consistent array-based data, improving reliability and compatibility across all import scenarios.

includes/class-blocks.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,8 @@ class="activitypub-modal__close wp-element-button wp-block-button__link"
279279
/**
280280
* Converts content to blocks before saving to the database.
281281
*
282-
* @param array $data The post data to be inserted.
283-
* @param object $post The Mastodon Create activity.
282+
* @param array $data The post data to be inserted.
283+
* @param array $post The Mastodon Create activity.
284284
*
285285
* @return array
286286
*/
@@ -297,8 +297,8 @@ function ( $paragraph ) {
297297
$data['post_content'] = \rtrim( \implode( PHP_EOL, $blocks ), PHP_EOL );
298298

299299
// Add reply block if it's a reply.
300-
if ( null !== $post->object->inReplyTo ) {
301-
$reply_block = \sprintf( '<!-- wp:activitypub/reply {"url":"%1$s","embedPost":true} /-->' . PHP_EOL, \esc_url( $post->object->inReplyTo ) );
300+
if ( ! empty( $post['object']['inReplyTo'] ) ) {
301+
$reply_block = \sprintf( '<!-- wp:activitypub/reply {"url":"%1$s","embedPost":true} /-->' . PHP_EOL, \esc_url( $post['object']['inReplyTo'] ) );
302302
$data['post_content'] = $reply_block . $data['post_content'];
303303
}
304304

includes/wp-admin/import/class-mastodon.php

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class Mastodon {
3333
/**
3434
* Outbox file.
3535
*
36-
* @var object
36+
* @var array
3737
*/
3838
private static $outbox;
3939

@@ -154,15 +154,15 @@ public static function handle_upload() {
154154
*/
155155
public static function import_options() {
156156
$author = 0;
157-
if ( isset( self::$outbox->{'orderedItems'}[0] ) ) {
157+
if ( isset( self::$outbox['orderedItems'][0] ) ) {
158158
$users = \get_users(
159159
array(
160160
'fields' => 'ID',
161161
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
162162
'meta_query' => array(
163163
array(
164164
'key' => $GLOBALS['wpdb']->get_blog_prefix() . 'activitypub_also_known_as',
165-
'value' => self::$outbox->{'orderedItems'}[0]->actor,
165+
'value' => self::$outbox['orderedItems'][0]['actor'],
166166
'compare' => 'LIKE',
167167
),
168168
),
@@ -233,7 +233,7 @@ public static function import() {
233233
}
234234

235235
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
236-
self::$outbox = \json_decode( \file_get_contents( self::$archive . '/outbox.json' ) );
236+
self::$outbox = \json_decode( \file_get_contents( self::$archive . '/outbox.json' ), true );
237237

238238
\wp_suspend_cache_invalidation();
239239
\wp_defer_term_counting( true );
@@ -278,43 +278,43 @@ public static function import_posts() {
278278
$skipped = array();
279279
$imported = 0;
280280

281-
foreach ( self::$outbox->{'orderedItems'} as $post ) {
281+
foreach ( self::$outbox['orderedItems'] as $post ) {
282282
// Skip boosts.
283-
if ( 'Announce' === $post->type ) {
283+
if ( 'Announce' === $post['type'] ) {
284284
continue;
285285
}
286286

287-
if ( ! is_activity_public( \get_object_vars( $post ) ) ) {
287+
if ( ! is_activity_public( $post ) ) {
288288
continue;
289289
}
290290

291291
// @todo: Skip replies to comments and import them as comments.
292292

293293
$post_data = array(
294294
'post_author' => self::$author,
295-
'post_date' => $post->published,
296-
'post_excerpt' => $post->object->summary ?? '',
297-
'post_content' => $post->object->content,
295+
'post_date' => $post['published'],
296+
'post_excerpt' => $post['object']['summary'] ?? '',
297+
'post_content' => $post['object']['content'],
298298
'post_status' => 'publish',
299299
'post_type' => 'post',
300-
'meta_input' => array( '_source_id' => $post->object->id ),
300+
'meta_input' => array( '_source_id' => $post['object']['id'] ),
301301
'tags_input' => \array_map(
302302
function ( $tag ) {
303-
if ( 'Hashtag' === $tag->type ) {
304-
return \ltrim( $tag->name, '#' );
303+
if ( 'Hashtag' === $tag['type'] ) {
304+
return \ltrim( $tag['name'], '#' );
305305
}
306306

307307
return '';
308308
},
309-
$post->object->tag
309+
$post['object']['tag'] ?? array()
310310
),
311311
);
312312

313313
/**
314314
* Filter the post data before inserting it into the database.
315315
*
316-
* @param array $post_data The post data to be inserted.
317-
* @param object $post The Mastodon Create activity.
316+
* @param array $post_data The post data to be inserted.
317+
* @param array $post The Mastodon Create activity.
318318
*/
319319
$post_data = \apply_filters( 'activitypub_import_mastodon_post_data', $post_data, $post );
320320

@@ -334,7 +334,7 @@ function ( $tag ) {
334334
$post_exists = \apply_filters( 'wp_import_existing_post', $post_exists, $post_data );
335335

336336
if ( $post_exists ) {
337-
$skipped[] = $post->object->id;
337+
$skipped[] = $post['object']['id'];
338338
continue;
339339
}
340340

@@ -347,15 +347,15 @@ function ( $tag ) {
347347
\set_post_format( $post_id, 'status' );
348348

349349
// Process attachments if enabled.
350-
if ( self::$fetch_attachments && ! empty( $post->object->attachment ) ) {
350+
if ( self::$fetch_attachments && ! empty( $post['object']['attachment'] ) ) {
351351
// Prepend archive path to attachment URLs for local files.
352-
$attachments = array_map( array( self::class, 'prepend_archive_path' ), $post->object->attachment );
352+
$attachments = array_map( array( self::class, 'prepend_archive_path' ), $post['object']['attachment'] );
353353

354354
Attachments::import( $attachments, $post_id, self::$author );
355355
}
356356

357357
// phpcs:ignore
358-
if ( $post_id && isset( $post->object->replies->first->next ) ) {
358+
if ( $post_id && isset( $post['object']['replies']['first']['next'] ) ) {
359359
// @todo: Import replies as comments.
360360
}
361361

@@ -423,13 +423,13 @@ public static function greet() {
423423
/**
424424
* Prepend archive path to local attachment URLs.
425425
*
426-
* @param object $attachment The attachment object.
426+
* @param array $attachment The attachment array.
427427
*
428-
* @return object The attachment object with updated URL.
428+
* @return array The attachment array with updated URL.
429429
*/
430430
private static function prepend_archive_path( $attachment ) {
431-
if ( ! empty( $attachment->url ) && ! preg_match( '#^https?://#i', $attachment->url ) ) {
432-
$attachment->url = self::$archive . $attachment->url;
431+
if ( ! empty( $attachment['url'] ) && ! preg_match( '#^https?://#i', $attachment['url'] ) ) {
432+
$attachment['url'] = self::$archive . $attachment['url'];
433433
}
434434

435435
return $attachment;

tests/phpunit/tests/includes/class-test-blocks.php

Lines changed: 192 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,8 @@ public function test_filter_import_mastodon_post_data_with_paragraphs() {
185185
'post_content' => '<p>First paragraph</p><p>Second paragraph</p>',
186186
);
187187

188-
$post = (object) array(
189-
'object' => (object) array(
188+
$post = array(
189+
'object' => array(
190190
'inReplyTo' => null,
191191
),
192192
);
@@ -207,8 +207,8 @@ public function test_filter_import_mastodon_post_data_with_reply() {
207207
);
208208

209209
$reply_url = 'https://mastodon.social/@user/123456';
210-
$post = (object) array(
211-
'object' => (object) array(
210+
$post = array(
211+
'object' => array(
212212
'inReplyTo' => $reply_url,
213213
),
214214
);
@@ -219,6 +219,194 @@ public function test_filter_import_mastodon_post_data_with_reply() {
219219
$this->assertStringContainsString( "<!-- wp:paragraph -->\n<p>This is a reply</p>\n<!-- /wp:paragraph -->", $result['post_content'] );
220220
}
221221

222+
/**
223+
* Test filter_import_mastodon_post_data without inReplyTo field.
224+
*
225+
* @covers ::filter_import_mastodon_post_data
226+
*/
227+
public function test_filter_import_mastodon_post_data_without_inreplyto() {
228+
$data = array(
229+
'post_content' => '<p>Regular post without reply</p>',
230+
);
231+
232+
$post = array(
233+
'object' => array(
234+
// No inReplyTo field.
235+
),
236+
);
237+
238+
$result = Blocks::filter_import_mastodon_post_data( $data, $post );
239+
240+
$this->assertStringNotContainsString( 'wp:activitypub/reply', $result['post_content'], 'Should not add reply block when no inReplyTo' );
241+
$this->assertStringContainsString( "<!-- wp:paragraph -->\n<p>Regular post without reply</p>\n<!-- /wp:paragraph -->", $result['post_content'] );
242+
}
243+
244+
/**
245+
* Test filter_import_mastodon_post_data with multiple paragraphs and a reply.
246+
*
247+
* @covers ::filter_import_mastodon_post_data
248+
*/
249+
public function test_filter_import_mastodon_post_data_with_multiple_paragraphs_and_reply() {
250+
$data = array(
251+
'post_content' => '<p>First paragraph</p><p>Second paragraph</p><p>Third paragraph</p>',
252+
);
253+
254+
$reply_url = 'https://mastodon.social/@alice/789';
255+
$post = array(
256+
'object' => array(
257+
'inReplyTo' => $reply_url,
258+
),
259+
);
260+
261+
$result = Blocks::filter_import_mastodon_post_data( $data, $post );
262+
263+
// Should have reply block at the start.
264+
$this->assertStringStartsWith( '<!-- wp:activitypub/reply', $result['post_content'], 'Reply block should be at the start' );
265+
266+
// Should have all three paragraphs as blocks.
267+
$this->assertStringContainsString( '<!-- wp:paragraph -->', $result['post_content'] );
268+
$this->assertSame( 3, substr_count( $result['post_content'], '<!-- wp:paragraph -->' ), 'Should have 3 paragraph blocks' );
269+
$this->assertSame( 3, substr_count( $result['post_content'], '<!-- /wp:paragraph -->' ), 'Should close 3 paragraph blocks' );
270+
}
271+
272+
/**
273+
* Test filter_import_mastodon_post_data with empty content.
274+
*
275+
* @covers ::filter_import_mastodon_post_data
276+
*/
277+
public function test_filter_import_mastodon_post_data_with_empty_content() {
278+
$data = array(
279+
'post_content' => '',
280+
);
281+
282+
$post = array(
283+
'object' => array(
284+
'inReplyTo' => null,
285+
),
286+
);
287+
288+
$result = Blocks::filter_import_mastodon_post_data( $data, $post );
289+
290+
// Should handle empty content gracefully.
291+
$this->assertSame( '', $result['post_content'], 'Should return empty string for empty content' );
292+
}
293+
294+
/**
295+
* Test filter_import_mastodon_post_data with content but no paragraph tags.
296+
*
297+
* @covers ::filter_import_mastodon_post_data
298+
*/
299+
public function test_filter_import_mastodon_post_data_with_non_paragraph_content() {
300+
$data = array(
301+
'post_content' => 'Plain text without paragraph tags',
302+
);
303+
304+
$post = array(
305+
'object' => array(
306+
'inReplyTo' => null,
307+
),
308+
);
309+
310+
$result = Blocks::filter_import_mastodon_post_data( $data, $post );
311+
312+
// Should handle content without <p> tags.
313+
$this->assertSame( '', $result['post_content'], 'Should return empty string when no paragraphs found' );
314+
}
315+
316+
/**
317+
* Test filter_import_mastodon_post_data preserves data keys.
318+
*
319+
* @covers ::filter_import_mastodon_post_data
320+
*/
321+
public function test_filter_import_mastodon_post_data_preserves_other_data() {
322+
$data = array(
323+
'post_content' => '<p>Test content</p>',
324+
'post_author' => 123,
325+
'post_date' => '2024-01-15T10:30:00Z',
326+
'post_excerpt' => 'Test excerpt',
327+
'meta_input' => array( '_source_id' => 'test-id' ),
328+
);
329+
330+
$post = array(
331+
'object' => array(
332+
'inReplyTo' => null,
333+
),
334+
);
335+
336+
$result = Blocks::filter_import_mastodon_post_data( $data, $post );
337+
338+
// Should preserve all other data keys.
339+
$this->assertArrayHasKey( 'post_author', $result, 'Should preserve post_author' );
340+
$this->assertSame( 123, $result['post_author'], 'Should preserve post_author value' );
341+
$this->assertArrayHasKey( 'post_date', $result, 'Should preserve post_date' );
342+
$this->assertSame( '2024-01-15T10:30:00Z', $result['post_date'], 'Should preserve post_date value' );
343+
$this->assertArrayHasKey( 'post_excerpt', $result, 'Should preserve post_excerpt' );
344+
$this->assertArrayHasKey( 'meta_input', $result, 'Should preserve meta_input' );
345+
346+
// Should only modify post_content.
347+
$this->assertNotSame( '<p>Test content</p>', $result['post_content'], 'Should modify post_content' );
348+
$this->assertStringContainsString( '<!-- wp:paragraph -->', $result['post_content'], 'Should add block markup' );
349+
}
350+
351+
/**
352+
* Test filter_import_mastodon_post_data with nested HTML in paragraphs.
353+
*
354+
* @covers ::filter_import_mastodon_post_data
355+
*/
356+
public function test_filter_import_mastodon_post_data_with_nested_html() {
357+
$data = array(
358+
'post_content' => '<p>Text with <a href="https://example.com">a link</a> and <strong>bold text</strong></p>',
359+
);
360+
361+
$post = array(
362+
'object' => array(
363+
'inReplyTo' => null,
364+
),
365+
);
366+
367+
$result = Blocks::filter_import_mastodon_post_data( $data, $post );
368+
369+
// Should preserve nested HTML.
370+
$this->assertStringContainsString( '<a href="https://example.com">a link</a>', $result['post_content'], 'Should preserve links' );
371+
$this->assertStringContainsString( '<strong>bold text</strong>', $result['post_content'], 'Should preserve strong tags' );
372+
$this->assertStringContainsString( '<!-- wp:paragraph -->', $result['post_content'], 'Should add block markup' );
373+
}
374+
375+
/**
376+
* Test filter_import_mastodon_post_data integration with array-based post data.
377+
*
378+
* @covers ::filter_import_mastodon_post_data
379+
*/
380+
public function test_filter_import_mastodon_post_data_with_complete_activity() {
381+
$data = array(
382+
'post_content' => '<p>Complete test</p>',
383+
);
384+
385+
// Realistic Mastodon activity structure.
386+
$post = array(
387+
'id' => 'https://mastodon.social/users/example/statuses/123/activity',
388+
'type' => 'Create',
389+
'actor' => 'https://mastodon.social/users/example',
390+
'published' => '2024-01-15T10:30:00Z',
391+
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
392+
'object' => array(
393+
'id' => 'https://mastodon.social/users/example/statuses/123',
394+
'type' => 'Note',
395+
'content' => '<p>Complete test</p>',
396+
'published' => '2024-01-15T10:30:00Z',
397+
'inReplyTo' => 'https://mastodon.social/@other/456',
398+
),
399+
);
400+
401+
$result = Blocks::filter_import_mastodon_post_data( $data, $post );
402+
403+
// Should work with complete activity structure.
404+
$this->assertIsArray( $result, 'Should return array' );
405+
$this->assertArrayHasKey( 'post_content', $result, 'Should have post_content key' );
406+
$this->assertStringContainsString( 'wp:activitypub/reply', $result['post_content'], 'Should add reply block' );
407+
$this->assertStringContainsString( '<!-- wp:paragraph -->', $result['post_content'], 'Should add paragraph block' );
408+
}
409+
222410
/**
223411
* Test the reactions block with deprecated markup.
224412
*/

0 commit comments

Comments
 (0)