Merge pull request 'Improve RSS compliance' (#7932) from fix-rss into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma/pulls/7932
This commit is contained in:
commit
c2b8c67808
13 changed files with 546 additions and 99 deletions
59
test/pleroma/web/feed/feed_view_test.exs
Normal file
59
test/pleroma/web/feed/feed_view_test.exs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Feed.FeedViewTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Web.Feed.FeedView
|
||||
|
||||
test "escapes fallback content" do
|
||||
data = %{
|
||||
"attachment" => [],
|
||||
"content" => "",
|
||||
"summary" => "<script>alert('feed')</script>"
|
||||
}
|
||||
|
||||
expected = "<script>alert('feed')</script>"
|
||||
|
||||
assert FeedView.atom_content_encoded(data) == expected
|
||||
assert FeedView.rss_content_encoded(data) == expected
|
||||
end
|
||||
|
||||
test "does not preview or classify sensitive attachments" do
|
||||
attachment = %{
|
||||
"name" => "spoiler",
|
||||
"url" => [%{"href" => "https://example.com/spoiler.png", "mediaType" => "image/png"}]
|
||||
}
|
||||
|
||||
data = %{
|
||||
"attachment" => [attachment],
|
||||
"content" => "content warning",
|
||||
"sensitive" => true
|
||||
}
|
||||
|
||||
assert FeedView.atom_content_encoded(data) == "content warning"
|
||||
assert FeedView.rss_content_encoded(data) == "content warning"
|
||||
assert FeedView.media_content_xml(attachment, true) == ""
|
||||
assert FeedView.media_content_xml(attachment, "true") == ""
|
||||
|
||||
media_content = FeedView.media_content_xml(attachment, false)
|
||||
assert media_content =~ "<media:description type=\"plain\">spoiler</media:description>"
|
||||
refute media_content =~ "media:rating"
|
||||
|
||||
assert FeedView.media_content_xml(Map.delete(attachment, "name"), false) ==
|
||||
~s(<media:content url="https://example.com/spoiler.png" type="image/png" medium="image"/>\n)
|
||||
end
|
||||
|
||||
test "does not treat a legacy false string as sensitive" do
|
||||
data = %{
|
||||
"attachment" => [
|
||||
%{"url" => [%{"href" => "https://example.com/image.png", "mediaType" => "image/png"}]}
|
||||
],
|
||||
"content" => "visible",
|
||||
"sensitive" => "false"
|
||||
}
|
||||
|
||||
assert FeedView.atom_content_encoded(data) =~ "<img"
|
||||
end
|
||||
end
|
||||
|
|
@ -30,9 +30,18 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
|
|||
%{
|
||||
"url" => [
|
||||
%{
|
||||
"href" =>
|
||||
"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
|
||||
"href" => "https://peertube.moe/video.mp4?first=one&second=two",
|
||||
"mediaType" => "video/mp4",
|
||||
"size" => "123",
|
||||
"type" => "Link"
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
"url" => [
|
||||
%{
|
||||
"href" => "https://peertube.moe/notes.pdf",
|
||||
"mediaType" => "application/pdf",
|
||||
"type" => "Link"
|
||||
}
|
||||
]
|
||||
|
|
@ -63,6 +72,15 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
|
|||
]
|
||||
|
||||
assert xpath(xml, ~x"//feed/entry/author/name/text()"ls) == [user.nickname, user.nickname]
|
||||
assert xpath(xml, ~x"//feed/entry/link[@rel='self']/@href"sl) == []
|
||||
assert length(xpath(xml, ~x"//feed/entry/link[@rel='alternate']/@href"sl)) == 2
|
||||
|
||||
assert xpath(xml, ~x"//feed/entry/link[@rel='enclosure']/@href"sl) == [
|
||||
"https://peertube.moe/video.mp4?first=one&second=two",
|
||||
"https://peertube.moe/notes.pdf"
|
||||
]
|
||||
|
||||
assert xpath(xml, ~x"//feed/entry/link[@rel='enclosure']/@length"sl) == ["123"]
|
||||
|
||||
conn =
|
||||
conn
|
||||
|
|
@ -96,17 +114,28 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
|
|||
%{
|
||||
"url" => [
|
||||
%{
|
||||
"href" =>
|
||||
"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
|
||||
"href" => "https://peertube.moe/video.mp4?first=one&second=two",
|
||||
"mediaType" => "video/mp4",
|
||||
"size" => "123",
|
||||
"type" => "Link"
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
"url" => [
|
||||
%{
|
||||
"href" => "https://peertube.moe/notes.pdf",
|
||||
"mediaType" => "application/pdf",
|
||||
"type" => "Link"
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
edited_at = NaiveDateTime.add(object.updated_at, 60)
|
||||
|
||||
object
|
||||
|> Ecto.Changeset.change(data: object_data)
|
||||
|> Ecto.Changeset.change(data: object_data, updated_at: edited_at)
|
||||
|> Pleroma.Repo.update()
|
||||
|
||||
{:ok, activity2} = CommonAPI.post(user, %{status: "42 This is :moominmamma #PleromaArt"})
|
||||
|
|
@ -136,15 +165,27 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
|
|||
~c"yeah #PleromaArt"
|
||||
]
|
||||
|
||||
assert xpath(xml, ~x"//channel/item/guid/@isPermaLink"sl) == ["true", "true"]
|
||||
|
||||
assert xpath(xml, ~x"//channel/item/pubDate/text()"sl) == [
|
||||
FeedView.to_rfc2822(activity2.data["published"]),
|
||||
FeedView.to_rfc2822(activity1.data["published"])
|
||||
]
|
||||
|
||||
assert xpath(xml, ~x"//channel/item/enclosure/@url"sl) == [
|
||||
"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4"
|
||||
assert xpath(xml, ~x"//channel/lastBuildDate/text()"s) ==
|
||||
FeedView.to_rfc2822(edited_at)
|
||||
|
||||
assert xpath(xml, ~x"//channel/item/media:content/@url"sl) == [
|
||||
"https://peertube.moe/video.mp4?first=one&second=two",
|
||||
"https://peertube.moe/notes.pdf"
|
||||
]
|
||||
|
||||
assert xpath(xml, ~x"//channel/item/enclosure/@url"sl) == [
|
||||
"https://peertube.moe/video.mp4?first=one&second=two"
|
||||
]
|
||||
|
||||
assert xpath(xml, ~x"//channel/item/enclosure/@length"sl) == ["123"]
|
||||
|
||||
obj1 = Object.normalize(activity1, fetch: false)
|
||||
obj2 = Object.normalize(activity2, fetch: false)
|
||||
|
||||
|
|
@ -181,6 +222,90 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
|
|||
]
|
||||
end
|
||||
|
||||
test "does not fabricate lastBuildDate for an empty RSS feed", %{conn: conn} do
|
||||
response =
|
||||
conn
|
||||
|> put_req_header("accept", "application/rss+xml")
|
||||
|> get(tag_feed_path(conn, :feed, "empty.rss"))
|
||||
|> response(200)
|
||||
|
||||
refute response =~ "<lastBuildDate>"
|
||||
end
|
||||
|
||||
test "provides a valid updated timestamp for an empty Atom feed", %{conn: conn} do
|
||||
response =
|
||||
conn
|
||||
|> put_req_header("accept", "application/atom+xml")
|
||||
|> get(tag_feed_path(conn, :feed, "empty.atom"))
|
||||
|> response(200)
|
||||
|
||||
updated = response |> parse() |> xpath(~x"//feed/updated/text()"s)
|
||||
assert {:ok, _datetime, 0} = DateTime.from_iso8601(updated)
|
||||
end
|
||||
|
||||
test "escapes Atom IDs and tag feed self links", %{conn: conn} do
|
||||
user = insert(:user, local: false)
|
||||
object_id = "https://remote.example/object?first=one&second=two"
|
||||
|
||||
note =
|
||||
insert(:note,
|
||||
user: user,
|
||||
object_local: false,
|
||||
data: %{
|
||||
"content" => "remote #atomlink",
|
||||
"external_url" => object_id,
|
||||
"id" => object_id,
|
||||
"summary" => "",
|
||||
"tag" => ["atomlink"]
|
||||
}
|
||||
)
|
||||
|
||||
insert(:note_activity, user: user, note: note, local: false, object_local: false)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> put_req_header("accept", "application/atom+xml")
|
||||
|> get("/tags/atomlink.atom?third=three&fourth=four")
|
||||
|> response(200)
|
||||
|
||||
assert response =~ "https://remote.example/object?first=one&second=two"
|
||||
assert response =~ "third=three&fourth=four"
|
||||
|
||||
xml = parse(response)
|
||||
assert xpath(xml, ~x"//feed/entry/id/text()"s) == object_id
|
||||
end
|
||||
|
||||
test "escapes and round-trips RSS item permalinks", %{conn: conn} do
|
||||
user = insert(:user, local: false)
|
||||
permalink = "https://remote.example/post?first=one&second=two"
|
||||
|
||||
note =
|
||||
insert(:note,
|
||||
user: user,
|
||||
object_local: false,
|
||||
data: %{
|
||||
"content" => "remote #rsslink",
|
||||
"external_url" => permalink,
|
||||
"summary" => "",
|
||||
"tag" => ["rsslink"]
|
||||
}
|
||||
)
|
||||
|
||||
insert(:note_activity, user: user, note: note, local: false, object_local: false)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> put_req_header("accept", "application/rss+xml")
|
||||
|> get(tag_feed_path(conn, :feed, "rsslink.rss"))
|
||||
|> response(200)
|
||||
|
||||
assert response =~ "https://remote.example/post?first=one&second=two"
|
||||
|
||||
xml = parse(response)
|
||||
assert xpath(xml, ~x"//channel/item/guid/text()"s) == permalink
|
||||
assert xpath(xml, ~x"//channel/item/link/text()"s) == permalink
|
||||
end
|
||||
|
||||
describe "private instance" do
|
||||
setup do: clear_config([:instance, :public], false)
|
||||
|
||||
|
|
|
|||
|
|
@ -83,12 +83,12 @@ defmodule Pleroma.Web.Feed.UserControllerTest do
|
|||
|> get(user_feed_path(conn, :feed, user.nickname))
|
||||
|> response(200)
|
||||
|
||||
activity_titles =
|
||||
resp
|
||||
|> SweetXml.parse()
|
||||
|> SweetXml.xpath(~x"//entry/title/text()"l)
|
||||
xml = SweetXml.parse(resp)
|
||||
activity_titles = SweetXml.xpath(xml, ~x"//entry/title/text()"l)
|
||||
|
||||
assert activity_titles == [~c"Won't, didn'...", ~c"2hu", ~c"2hu & as"]
|
||||
assert SweetXml.xpath(xml, ~x"//entry/link[@rel='self']/@href"sl) == []
|
||||
assert length(SweetXml.xpath(xml, ~x"//entry/link[@rel='alternate']/@href"sl)) == 3
|
||||
assert resp =~ FeedView.escape(object.data["content"])
|
||||
assert resp =~ FeedView.escape(object.data["summary"])
|
||||
assert resp =~ FeedView.escape(object.data["context"])
|
||||
|
|
@ -138,6 +138,107 @@ defmodule Pleroma.Web.Feed.UserControllerTest do
|
|||
assert activity_titles == [~c"2hu & as"]
|
||||
end
|
||||
|
||||
test "escapes query parameters in feed self links", %{conn: conn, user: user} do
|
||||
for format <- ~w[atom rss] do
|
||||
response =
|
||||
conn
|
||||
|> get("/users/#{user.nickname}/feed.#{format}?first=one&second=two")
|
||||
|> response(200)
|
||||
|
||||
assert response =~ "first=one&second=two"
|
||||
refute response =~ "first=one&second=two"
|
||||
end
|
||||
end
|
||||
|
||||
test "keeps fallback summary markup inert in rendered Atom content", %{
|
||||
conn: conn,
|
||||
object: object,
|
||||
user: user
|
||||
} do
|
||||
data =
|
||||
object.data
|
||||
|> Map.put("content", "")
|
||||
|> Map.put("summary", "<script>alert('feed')</script>")
|
||||
|
||||
object
|
||||
|> Ecto.Changeset.change(data: data)
|
||||
|> Pleroma.Repo.update!()
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get("/users/#{user.nickname}/feed.atom")
|
||||
|> response(200)
|
||||
|
||||
assert response =~
|
||||
"&lt;script&gt;alert(&#39;feed&#39;)&lt;/script&gt;"
|
||||
|
||||
content_values = response |> parse() |> xpath(~x"//entry/content/text()"sl)
|
||||
|
||||
assert Enum.any?(content_values, fn content ->
|
||||
String.starts_with?(content, "<script>alert('feed')</script>")
|
||||
end)
|
||||
end
|
||||
|
||||
test "does not expose sensitive attachments as enclosures", %{
|
||||
conn: conn,
|
||||
object: object,
|
||||
user: user
|
||||
} do
|
||||
object
|
||||
|> Ecto.Changeset.change(data: Map.put(object.data, "sensitive", true))
|
||||
|> Pleroma.Repo.update!()
|
||||
|
||||
atom_response =
|
||||
conn
|
||||
|> get("/users/#{user.nickname}/feed.atom")
|
||||
|> response(200)
|
||||
|
||||
rss_response =
|
||||
conn
|
||||
|> get("/users/#{user.nickname}/feed.rss")
|
||||
|> response(200)
|
||||
|
||||
refute atom_response =~ "rel=\"enclosure\""
|
||||
refute rss_response =~ "<enclosure "
|
||||
end
|
||||
|
||||
test "uses enclosure fallbacks for user feeds", %{conn: conn, object: object, user: user} do
|
||||
attachment = %{"url" => [%{"href" => "https://example.com/file.bin"}]}
|
||||
|
||||
object
|
||||
|> Ecto.Changeset.change(data: Map.put(object.data, "attachment", [attachment]))
|
||||
|> Pleroma.Repo.update!()
|
||||
|
||||
atom_xml =
|
||||
conn
|
||||
|> get("/users/#{user.nickname}/feed.atom")
|
||||
|> response(200)
|
||||
|> parse()
|
||||
|
||||
assert xpath(atom_xml, ~x"//entry/link[@rel='enclosure']/@href"sl) == [
|
||||
"https://example.com/file.bin"
|
||||
]
|
||||
|
||||
assert xpath(atom_xml, ~x"//entry/link[@rel='enclosure']/@type"sl) == [
|
||||
"application/octet-stream"
|
||||
]
|
||||
|
||||
assert xpath(atom_xml, ~x"//entry/link[@rel='enclosure']/@length"sl) == []
|
||||
|
||||
rss_xml =
|
||||
conn
|
||||
|> get("/users/#{user.nickname}/feed.rss")
|
||||
|> response(200)
|
||||
|> parse()
|
||||
|
||||
assert xpath(rss_xml, ~x"//item/enclosure/@url"sl) == []
|
||||
assert xpath(rss_xml, ~x"//item/media:content/@url"sl) == ["https://example.com/file.bin"]
|
||||
|
||||
assert xpath(rss_xml, ~x"//item/media:content/@type"sl) == [
|
||||
"application/octet-stream"
|
||||
]
|
||||
end
|
||||
|
||||
test "returns 404 for a missing feed", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue