From 5cd4454f5491a0a7d31b8bb38ef2a4266f4a7f3a Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 29 Nov 2024 09:23:32 +0100 Subject: [PATCH] OPML Import: Support OPML without nesting (#403) --- includes/class-import.php | 118 +++++++++++++++++++++++++------------- includes/class-user.php | 8 ++- tests/data/feedland.opml | 44 ++++++++++++++ tests/data/friends.opml | 37 ++++++++++++ tests/test-feed.php | 28 +++++++++ 5 files changed, 192 insertions(+), 43 deletions(-) create mode 100644 tests/data/feedland.opml create mode 100644 tests/data/friends.opml diff --git a/includes/class-import.php b/includes/class-import.php index 6a4a0efc..7be8bebe 100644 --- a/includes/class-import.php +++ b/includes/class-import.php @@ -18,57 +18,95 @@ * @author Alex Kirk */ class Import { - public static function opml( $opml ) { - $opml = simplexml_load_string( $opml ); - if ( ! $opml ) { - return new \WP_Error( 'friends_import_opml_error', __( 'Failed to parse OPML.', 'friends' ) ); + + private static function get_feed_from_opml_node( $friend ) { + $xml_url = (string) $friend['xmlUrl']; + if ( ! $xml_url ) { + return null; } - $feeds = array(); - foreach ( $opml->body->outline as $outline ) { - $role = (string) $outline['text']; - foreach ( $outline->outline as $friend ) { - $user_login = str_replace( ' ', '-', strtolower( sanitize_user( (string) $friend['text'] ) ) ); - $feed = User_Feed::get_by_url( (string) $friend['xmlUrl'] ); - if ( $feed instanceof User_Feed ) { - $friend_user = $feed->get_friend_user(); - if ( ! isset( $feeds[ $friend_user->user_login ] ) ) { - $feeds[ $friend_user->user_login ] = array(); - } - $feeds[ $friend_user->user_login ][] = $feed; - continue; - } + $username = (string) $friend['text']; + if ( ! $username ) { + $username = (string) $friend['title']; + } + if ( ! $username ) { + $username = (string) $friend['htmlUrl']; + $username = preg_replace( '/^https?:\/\//', '', $username ); + } + if ( ! $username ) { + $username = (string) $xml_url; + $username = preg_replace( '/^https?:\/\//', '', $username ); + } - $friend_user = User::get_by_username( $user_login ); - if ( ! $friend_user || is_wp_error( $friend_user ) ) { - $friend_user = Subscription::create( - $user_login, - 'subscription', - (string) $friend['htmlUrl'], - (string) $friend['text'] - ); - } - if ( ! $friend_user instanceof User ) { - continue; - } - $feed = $friend_user->save_feed( - (string) $friend['xmlUrl'], - array( - 'active' => true, - 'mime-type' => 'atom' === (string) $friend['type'] ? 'application/atom+xml' : 'application/rss+xml', - ) - ); - if ( ! $feed instanceof User_Feed ) { - continue; - } + $user_login = User::sanitize_username( $username ); + $feed = User_Feed::get_by_url( $xml_url ); + if ( $feed instanceof User_Feed ) { + return $feed; + } + + $friend_user = User::get_by_username( $user_login ); + if ( ! $friend_user || is_wp_error( $friend_user ) ) { + $friend_user = Subscription::create( + $user_login, + 'subscription', + (string) $friend['htmlUrl'], + (string) $friend['text'] + ); + } + + if ( ! $friend_user instanceof User ) { + return null; + } + + $feed = $friend_user->save_feed( + $xml_url, + array( + 'active' => true, + 'mime-type' => 'atom' === (string) $friend['type'] ? 'application/atom+xml' : 'application/rss+xml', + ) + ); + + if ( ! $feed instanceof User_Feed ) { + if ( is_wp_error( $feed ) && apply_filters( 'friends_debug', false ) ) { + wp_trigger_error( __FUNCTION__, $feed->get_error_message() ); + + } + return null; + } + + return $feed; + } + + private static function recurse_into_opml( $feeds, $friend ) { + $xml_url = (string) $friend['xmlUrl']; + if ( $xml_url ) { + $feed = self::get_feed_from_opml_node( $friend ); + if ( $feed instanceof User_Feed ) { + $friend_user = $feed->get_friend_user(); if ( ! isset( $feeds[ $friend_user->user_login ] ) ) { $feeds[ $friend_user->user_login ] = array(); } $feeds[ $friend_user->user_login ][] = $feed; } + return $feeds; + } + + foreach ( $friend->outline as $child ) { + $feeds = self::recurse_into_opml( $feeds, $child ); + } + + return $feeds; + } + + public static function opml( $opml ) { + $opml = simplexml_load_string( $opml ); + if ( ! $opml ) { + return new \WP_Error( 'friends_import_opml_error', __( 'Failed to parse OPML.', 'friends' ) ); } + $feeds = self::recurse_into_opml( array(), $opml->body ); + return $feeds; } } diff --git a/includes/class-user.php b/includes/class-user.php index e56f4b15..73c038b3 100644 --- a/includes/class-user.php +++ b/includes/class-user.php @@ -388,7 +388,7 @@ public function save_feeds( $feeds = array() ) { $errors = new \WP_Error(); foreach ( $feeds as $feed_url => $options ) { if ( ! is_string( $feed_url ) || ! Friends::check_url( $feed_url ) ) { - $errors->add( 'invalid-url', 'An invalid URL was provided' ); + $errors->add( 'invalid-url', 'An invalid URL was provided', $feed_url ); unset( $feeds[ $feed_url ] ); continue; } @@ -406,7 +406,8 @@ public function save_feeds( $feeds = array() ) { $all_urls = array(); foreach ( wp_get_object_terms( $this->get_object_id(), User_Feed::TAXONOMY ) as $term ) { - $all_urls[ $term->name ] = $term->term_id; + $url = str_replace( '&', '&', $term->name ); + $all_urls[ $url ] = $term->term_id; } $user_feeds = wp_set_object_terms( $this->get_object_id(), array_keys( array_merge( $all_urls, $feeds ) ), User_Feed::TAXONOMY ); @@ -415,7 +416,8 @@ public function save_feeds( $feeds = array() ) { } foreach ( wp_get_object_terms( $this->get_object_id(), User_Feed::TAXONOMY ) as $term ) { - $all_urls[ $term->name ] = $term->term_id; + $url = str_replace( '&', '&', $term->name ); + $all_urls[ $url ] = $term->term_id; } foreach ( $feeds as $url => $feed_options ) { diff --git a/tests/data/feedland.opml b/tests/data/feedland.opml new file mode 100644 index 00000000..9a7ff876 --- /dev/null +++ b/tests/data/feedland.opml @@ -0,0 +1,44 @@ + + + + bph/wordpress + bph's subscription list, wordpress category. List created by feedlandDatabase v0.7.55. + Thu, 28 Nov 2024 16:14:43 GMT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/data/friends.opml b/tests/data/friends.opml new file mode 100644 index 00000000..8f46a522 --- /dev/null +++ b/tests/data/friends.opml @@ -0,0 +1,37 @@ + + + My Friends + Fri, 29 Nov 2024 06:47:29 +0000 + adm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test-feed.php b/tests/test-feed.php index f27aa899..e989e2b9 100644 --- a/tests/test-feed.php +++ b/tests/test-feed.php @@ -678,4 +678,32 @@ public function test_podcast() { $this->assertEquals( 'https://podcast.local/2022/10/episode-1/', $post->guid ); $this->assertStringContainsString( 'first-episode.mp3', $post->post_content ); } + + public function test_import_feedland_opml() { + add_filter( 'friends_pre_check_url', '__return_true' ); + + $opml = file_get_contents( __DIR__ . '/data/feedland.opml' ); + $feeds = Import::opml( $opml ); + $users_created = count( $feeds ); + $feeds_imported = 0; + foreach ( $feeds as $user => $user_feeds ) { + $feeds_imported += count( $user_feeds ); + } + $this->assertEquals( 34, $users_created ); + $this->assertEquals( 34, $feeds_imported ); + } + + public function test_import_friends_opml() { + add_filter( 'friends_pre_check_url', '__return_true' ); + + $opml = file_get_contents( __DIR__ . '/data/friends.opml' ); + $feeds = Import::opml( $opml ); + $users_created = count( $feeds ); + $feeds_imported = 0; + foreach ( $feeds as $user => $user_feeds ) { + $feeds_imported += count( $user_feeds ); + } + $this->assertEquals( 22, $users_created ); + $this->assertEquals( 24, $feeds_imported ); + } }