changeset 14:376a0e415bba

Properly handle non-text content in Atom feed elements The title, subtitle, summary and content elements of Atom feeds can all have non-text content. When parsing title and subtitle elements HTML and XHTML content will be stripped of any markup in order to keep it simple. In summary and content elements markup will be preserved. Element content of any other type as well as remote content in content elements will be ignored.
author Guido Berhoerster <guido+feed-preview@berhoerster.name>
date Mon, 10 Dec 2018 16:38:11 +0100
parents 799d633ccd4d
children 150f07c7595f
files js/background.js js/feed-parser.js
diffstat 2 files changed, 56 insertions(+), 22 deletions(-) [+]
line wrap: on
line diff
--- a/js/background.js	Sat Dec 08 12:12:18 2018 +0100
+++ b/js/background.js	Mon Dec 10 16:38:11 2018 +0100
@@ -48,7 +48,10 @@
 const FEED_MAGIC = [
     '<rss',
     '<feed',
-    ...Object.values(feedParser.XMLNS)
+    feedParser.XMLNS.ATOM03,
+    feedParser.XMLNS.ATOM10,
+    feedParser.XMLNS.RSS09,
+    feedParser.XMLNS.RSS10
 ];
 var tabsFeeds = new Map();
 var tabsFeedPreviews = new Map();
--- a/js/feed-parser.js	Sat Dec 08 12:12:18 2018 +0100
+++ b/js/feed-parser.js	Mon Dec 10 16:38:11 2018 +0100
@@ -10,7 +10,8 @@
 
 export const XMLNS = {
     ATOM10: 'http://www.w3.org/2005/Atom',
-    RSS09: 'http://my.netscape.com/rdf/simple/0.9/'
+    RSS09: 'http://my.netscape.com/rdf/simple/0.9/',
+    XHTML: 'http://www.w3.org/1999/xhtml'
 }
 const ALLOWED_LINK_PROTOCOLS = new Set(['http:', 'https:', 'ftp:']);
 
@@ -216,6 +217,46 @@
         return new FeedLogo(url);
     }
 
+    parseAtomTextConstruct(containerElement, textOnly = true) {
+        let contentType = containerElement.getAttribute('type');
+        if (contentType === null) {
+            contentType = 'text';
+        }
+
+        if (contentType === 'xhtml') {
+            let xhtmlRootElement = containerElement.firstElementChild;
+            if (xhtmlRootElement !== null &&
+                    xhtmlRootElement.localName === 'div' &&
+                    xhtmlRootElement.namespaceURI === XMLNS.XHTML) {
+                return textOnly ? xhtmlRootElement.textContent.trim() :
+                        xhtmlRootElement.innerHTML;
+            }
+        } else if (contentType === 'html') {
+            let htmlText = containerElement.textContent;
+            if (textOnly) {
+                let htmlDocument = new DOMParser().parseFromString(htmlText,
+                        'text/html');
+                return htmlDocument.body.textContent.trim();
+            }
+            return htmlText
+        } else if (contentType === 'text') {
+            let text = containerElement.textContent.trim();
+            return textOnly ? text : `<pre>${encodeXML(text)}</pre>`;
+        }
+
+        // unsupported content type
+        return;
+    }
+
+    parseAtomContent(contentElement) {
+        let contentSrc = contentElement.getAttribute('src');
+        if (contentSrc !== null) {
+            // externally referenced content is not supported
+            return;
+        }
+        return this.parseAtomTextConstruct(contentElement, false);
+    }
+
     parseAtomEntry(entryElement) {
         let title;
         let link;
@@ -224,7 +265,7 @@
         let titleElement = feedQueryXPath(this.document, entryElement,
                 './atom:title');
         if (titleElement !== null) {
-            title = titleElement.textContent.trim();
+            title = this.parseAtomTextConstruct(titleElement);
         }
 
         let linkElement = feedQueryXPath(this.document, entryElement,
@@ -241,24 +282,14 @@
 
         let contentElement = feedQueryXPath(this.document, entryElement,
                 './atom:content');
-        if (contentElement === null) {
-            contentElement = feedQueryXPath(this.document, entryElement,
-                    './atom:summary');
+        if (contentElement !== null) {
+            content = this.parseAtomContent(contentElement);
         }
-        if (contentElement !== null) {
-            let contentType = contentElement.getAttribute('type');
-            if (contentType === null) {
-                contentType = 'text';
-            }
-            contentType = contentType.toLowerCase();
-            if (contentType === 'xhtml') {
-                content = contentElement.innerHTML;
-            } else if (contentType === 'html') {
-                content = contentElement.textContent;
-            } else {
-                let encodedContent =
-                        encodeXML(contentElement.textContent.trim());
-                content = `<pre>${encodedContent}</pre>`;
+        if (typeof content === 'undefined') {
+            let summaryElement = feedQueryXPath(this.document, entryElement,
+                    './atom:summary');
+            if (summaryElement !== null) {
+                content = this.parseAtomTextConstruct(summaryElement, false);
             }
         }
 
@@ -275,13 +306,13 @@
         let titleElement = feedQueryXPath(this.document, documentElement,
                 './atom:title');
         if (titleElement !== null) {
-            title = titleElement.textContent.trim();
+            title = this.parseAtomTextConstruct(titleElement);
         }
 
         let subtitleElement = feedQueryXPath(this.document, documentElement,
                 './atom:subtitle');
         if (subtitleElement !== null) {
-            subtitle = subtitleElement.textContent.trim();
+            subtitle = this.parseAtomTextConstruct(subtitleElement);
         }
 
         let logoElement = feedQueryXPath(this.document, documentElement,