summaryrefslogtreecommitdiff
path: root/django/utils
diff options
context:
space:
mode:
authorBaptiste Mispelon <bmispelon@gmail.com>2024-04-26 22:10:40 +0200
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2024-06-18 17:25:43 +0200
commit62300b81cf7687d176af3b40aa6101942801292d (patch)
tree12190b41d01e55f0799a439a6a690d7c644e0464 /django/utils
parentce1ad98565d98b7939367b2bcab755c3555ceb42 (diff)
Fixed #12978 -- Added support for RSS feed stylesheets.
Diffstat (limited to 'django/utils')
-rw-r--r--django/utils/feedgenerator.py74
1 files changed, 74 insertions, 0 deletions
diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py
index 3bd456ca68..fae3271430 100644
--- a/django/utils/feedgenerator.py
+++ b/django/utils/feedgenerator.py
@@ -24,6 +24,7 @@ https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004
import datetime
import email
+import mimetypes
from io import StringIO
from urllib.parse import urlparse
@@ -57,6 +58,53 @@ def get_tag_uri(url, date):
return "tag:%s%s:%s/%s" % (bits.hostname, d, bits.path, bits.fragment)
+def _guess_stylesheet_mimetype(url):
+ """
+ Return the given stylesheet's mimetype tuple, using a slightly custom
+ version of Python's mimetypes.guess_type().
+ """
+ mimetypedb = mimetypes.MimeTypes()
+
+ # The official mimetype for XSLT files is technically `application/xslt+xml`
+ # but as of 2024 almost no browser supports that (they all expect text/xsl).
+ # On top of that, windows seems to assume that the type for xsl is text/xml.
+ mimetypedb.readfp(StringIO("text/xsl\txsl\ntext/xsl\txslt"))
+
+ return mimetypedb.guess_type(url)
+
+
+class Stylesheet:
+ """An RSS stylesheet"""
+
+ def __init__(self, url, mimetype="", media="screen"):
+ self._url = url
+ self._mimetype = mimetype
+ self.media = media
+
+ # Using a property to delay the evaluation of self._url as late as possible
+ # in case of a lazy object (like reverse_lazy(...) for example).
+ @property
+ def url(self):
+ return iri_to_uri(self._url)
+
+ @property
+ def mimetype(self):
+ if self._mimetype == "":
+ return _guess_stylesheet_mimetype(self.url)[0]
+ return self._mimetype
+
+ def __str__(self):
+ data = [f'href="{self.url}"']
+ if self.mimetype is not None:
+ data.append(f'type="{self.mimetype}"')
+ if self.media is not None:
+ data.append(f'media="{self.media}"')
+ return " ".join(data)
+
+ def __repr__(self):
+ return repr((self.url, self.mimetype, self.media))
+
+
class SyndicationFeed:
"Base class for all syndication feeds. Subclasses should provide write()"
@@ -75,12 +123,24 @@ class SyndicationFeed:
feed_copyright=None,
feed_guid=None,
ttl=None,
+ stylesheets=None,
**kwargs,
):
def to_str(s):
return str(s) if s is not None else s
+ def to_stylesheet(s):
+ return s if isinstance(s, Stylesheet) else Stylesheet(s)
+
categories = categories and [str(c) for c in categories]
+
+ if stylesheets is not None:
+ if isinstance(stylesheets, (Stylesheet, str)):
+ raise TypeError(
+ f"stylesheets should be a list, not {stylesheets.__class__}"
+ )
+ stylesheets = [to_stylesheet(s) for s in stylesheets]
+
self.feed = {
"title": to_str(title),
"link": iri_to_uri(link),
@@ -95,6 +155,7 @@ class SyndicationFeed:
"feed_copyright": to_str(feed_copyright),
"id": feed_guid or link,
"ttl": to_str(ttl),
+ "stylesheets": stylesheets,
**kwargs,
}
self.items = []
@@ -166,6 +227,12 @@ class SyndicationFeed:
"""
pass
+ def add_stylesheets(self, handler):
+ """
+ Add stylesheet(s) to the feed. Called from write().
+ """
+ pass
+
def item_attributes(self, item):
"""
Return extra attributes to place on each item (i.e. item/entry) element.
@@ -228,6 +295,9 @@ class RssFeed(SyndicationFeed):
def write(self, outfile, encoding):
handler = SimplerXMLGenerator(outfile, encoding, short_empty_elements=True)
handler.startDocument()
+ # Any stylesheet must come after the start of the document but before any tag.
+ # https://www.w3.org/Style/styling-XML.en.html
+ self.add_stylesheets(handler)
handler.startElement("rss", self.rss_attributes())
handler.startElement("channel", self.root_attributes())
self.add_root_elements(handler)
@@ -247,6 +317,10 @@ class RssFeed(SyndicationFeed):
self.add_item_elements(handler, item)
handler.endElement("item")
+ def add_stylesheets(self, handler):
+ for stylesheet in self.feed["stylesheets"] or []:
+ handler.processingInstruction("xml-stylesheet", stylesheet)
+
def add_root_elements(self, handler):
handler.addQuickElement("title", self.feed["title"])
handler.addQuickElement("link", self.feed["link"])