-
Notifications
You must be signed in to change notification settings - Fork 435
Use beautifulsoup4 instead of lxml for URL previews
#19301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
6dec726
8e9e333
a24d251
d5332b0
18d2746
3813537
5940217
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1 @@ | ||||||
| Switch to beautofulsoup4 from lxml for URL previews. Controbuted by @clokep. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Linting/CI is not passing for typechecking ❌: https://github.com/element-hq/synapse/actions/runs/20170566919/job/57904924809?pr=19301
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I can't see to get the same setup locally. Did something change with how to install the pinned packages?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Things have changed behind the scenes but shouldn't affect how you install as a developer:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's what I did. Maybe I'll try creating a new virtualenv. 🤔
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't evaluated whether
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. beautifulsoup is the go to package when parsing HTML in Python and has been for at least a decade. |
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -633,10 +633,6 @@ This is critical from a security perspective to stop arbitrary Matrix users | |||||||||||||||||||||||||
| spidering 'internal' URLs on your network. At the very least we recommend that | ||||||||||||||||||||||||||
| your loopback and RFC1918 IP addresses are blacklisted. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| This also requires the optional `lxml` python dependency to be installed. This | ||||||||||||||||||||||||||
| in turn requires the `libxml2` library to be available - on Debian/Ubuntu this | ||||||||||||||||||||||||||
| means `apt-get install libxml2-dev`, or equivalent for your OS. | ||||||||||||||||||||||||||
|
Comment on lines
-636
to
-638
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a note, we can remove Line 103 in f79acff
Comment on lines
-636
to
-638
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can remove the synapse/.github/workflows/tests.yml Lines 443 to 448 in f79acff
synapse/.github/workflows/tests.yml Line 500 in f79acff
Comment on lines
-636
to
-638
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
synapse/docs/setup/installation.md Lines 308 to 311 in f79acff
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ### Backups | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Don't forget to take [backups](../usage/administration/backups.md) of your new server! | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -25,12 +25,12 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import attr | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from synapse.media.preview_html import parse_html_description | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from synapse.media.preview_html import NON_BLANK, decode_body, parse_html_description | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from synapse.types import JsonDict | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from synapse.util.json import json_decoder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if TYPE_CHECKING: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from lxml import etree | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from bs4 import BeautifulSoup | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from synapse.server import HomeServer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -105,35 +105,25 @@ def get_oembed_url(self, url: str) -> str | None: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # No match. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def autodiscover_from_html(self, tree: "etree._Element") -> str | None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def autodiscover_from_html(self, soup: "BeautifulSoup") -> str | None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Search an HTML document for oEmbed autodiscovery information. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tree: The parsed HTML body. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| soup: The parsed HTML body. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| The URL to use for oEmbed information, or None if no URL was found. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Search for link elements with the proper rel and type attributes. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for tag in cast( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| list["etree._Element"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tree.xpath("//link[@rel='alternate'][@type='application/json+oembed']"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "href" in tag.attrib: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cast(str, tag.attrib["href"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Some providers (e.g. Flickr) use alternative instead of alternate. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
We should move this above the |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for tag in cast( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| list["etree._Element"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tree.xpath("//link[@rel='alternative'][@type='application/json+oembed']"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "href" in tag.attrib: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cast(str, tag.attrib["href"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tag = soup.find( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "link", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rel=("alternate", "alternative"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where can I find this syntax? I've looked through https://beautiful-soup-4.readthedocs.io/en/latest/#searching-the-tree |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="application/json+oembed", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| href=NON_BLANK, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cast(str, tag["href"]) if tag else None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Safer lookups to avoid prior code needing assumptions:
Suggested change
Prior art but ideally, we'd do even better:
Suggested change
Also applies elsewhere below |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def parse_oembed_response(self, url: str, raw_body: bytes) -> OEmbedResult: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -196,7 +186,7 @@ def parse_oembed_response(self, url: str, raw_body: bytes) -> OEmbedResult: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if oembed_type == "rich": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| html_str = oembed.get("html") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(html_str, str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| calc_description_and_urls(open_graph_response, html_str) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| calc_description_and_urls(open_graph_response, html_str, url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif oembed_type == "photo": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # If this is a photo, use the full image, not the thumbnail. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -208,7 +198,7 @@ def parse_oembed_response(self, url: str, raw_body: bytes) -> OEmbedResult: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| open_graph_response["og:type"] = "video.other" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| html_str = oembed.get("html") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if html_str and isinstance(html_str, str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| calc_description_and_urls(open_graph_response, oembed["html"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| calc_description_and_urls(open_graph_response, oembed["html"], url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for size in ("width", "height"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val = oembed.get(size) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if type(val) is int: # noqa: E721 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -223,55 +213,45 @@ def parse_oembed_response(self, url: str, raw_body: bytes) -> OEmbedResult: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return OEmbedResult(open_graph_response, author_name, cache_age) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _fetch_urls(tree: "etree._Element", tag_name: str) -> list[str]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| results = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for tag in cast(list["etree._Element"], tree.xpath("//*/" + tag_name)): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "src" in tag.attrib: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| results.append(cast(str, tag.attrib["src"])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return results | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _fetch_url(soup: "BeautifulSoup", tag_name: str) -> str | None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tag = soup.find(tag_name, src=NON_BLANK) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cast(str, tag["src"]) if tag else None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def calc_description_and_urls(open_graph_response: JsonDict, html_body: str) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def calc_description_and_urls( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| open_graph_response: JsonDict, html_body: str, url: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Calculate description for an HTML document. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| This uses lxml to convert the HTML document into plaintext. If errors | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| This uses BeautifulSoup to convert the HTML document into plaintext. If errors | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| occur during processing of the document, an empty response is returned. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| open_graph_response: The current Open Graph summary. This is updated with additional fields. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| html_body: The HTML document, as bytes. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| The summary | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url: The URL which is being previewed (not the one which was requested). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # If there's no body, nothing useful is going to be found. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not html_body: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| soup = decode_body(html_body, url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from lxml import etree | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Create an HTML parser. If this fails, log and return no metadata. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parser = etree.HTMLParser(recover=True, encoding="utf-8") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Attempt to parse the body. If this fails, log and return no metadata. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tree = etree.fromstring(html_body, parser) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # The data was successfully parsed, but no tree was found. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if tree is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # If there's no body, nothing useful is going to be found. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not soup: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Attempt to find interesting URLs (images, videos, embeds). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if "og:image" not in open_graph_response: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| image_urls = _fetch_urls(tree, "img") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if image_urls: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| open_graph_response["og:image"] = image_urls[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| video_urls = _fetch_urls(tree, "video") + _fetch_urls(tree, "embed") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if video_urls: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| open_graph_response["og:video"] = video_urls[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description = parse_html_description(tree) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| image_url = _fetch_url(soup, "img") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if image_url: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| open_graph_response["og:image"] = image_url | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| video_url = _fetch_url(soup, "video") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if video_url: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| open_graph_response["og:video"] = video_url | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| embed_url = _fetch_url(soup, "embed") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if embed_url: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| open_graph_response["og:video"] = embed_url | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description = parse_html_description(soup) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if description: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| open_graph_response["og:description"] = description | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conflicts to resolve