Repository: primefaces/primefaces (java)

Issue:
FeedReader: Switch from Rome to rssreader library
> RSS Reader is a simple Java library for reading RSS and Atom feeds. It has zero 3rd party dependencies, a low memory footprint and can process large feeds. 

https://github.com/w3stling/rssreader

Reasons:

- [x] Zero dependencies unlike ROME which has dependencies on JDOM etc
- [x] Up to date last commit 3 weeks ago vs Rome which seems inactive
- [x] This should be transparent to most users as Rome was used in the component and not exposed.
- [x] Has special handling for Apple Itune Podcast RSS feeds
- [x] Does not XML parse but manually parses the documents to prevent previous XML related security issues like https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-33813 


Pull request description:
Fix #11789: FeedReader switch to new library
Fix #11789: FeedReader switch to new library

- [x] Zero dependencies unlike ROME which has dependencies on JDOM etc
- [x] Up to date last commit 3 weeks ago vs Rome which seems inactive
- [x] This should be transparent to most users as Rome was used in the component and not exposed.
- [x] Has special handling for Apple Itune Podcast RSS feeds
- [x] Does not XML parse but manually parses the documents to prevent previous XML related security issues like https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-33813 
- [x] Quarkus friendly!
- [x] Integration tests updated and passing

Patch:
diff --git a/README.md b/README.md
index a42c087047..4a5c9d3575 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ What does that mean?
 Version | Binary | Source | JSF version | Java version | Documentation
 ------------ | -------------  | ------------- | ------------- | ------------- | ------------- |
 ![14.0.x](https://img.shields.io/maven-central/v/org.primefaces/primefaces.svg?versionPrefix=14&color=cyan)| [JAR](https://search.maven.org/remotecontent?filepath=org/primefaces/primefaces/14.0.0/primefaces-14.0.0.jar)  | [Sources](https://search.maven.org/remotecontent?filepath=org/primefaces/primefaces/14.0.0/primefaces-14.0.0-sources.jar)| 2.3 - 4.0 | 11 - ? | [14.0.0 Documentation](https://primefaces.github.io/primefaces/14_0_0/#/)
-![13.0.x](https://img.shields.io/maven-central/v/org.primefaces/primefaces.svg?versionPrefix=13&color=cyan)| [JAR](https://search.maven.org/remotecontent?filepath=org/primefaces/primefaces/13.0.7/primefaces-13.0.7.jar)  | [Sources](https://search.maven.org/remotecontent?filepath=org/primefaces/primefaces/13.0.7/primefaces-13.0.7-sources.jar) | 2.0 - 4.0 | 8 - ? | [13.0.7 Documentation](https://primefaces.github.io/primefaces/13_0_0/#/)
+![13.0.x](https://img.shields.io/maven-central/v/org.primefaces/primefaces.svg?versionPrefix=13&color=cyan)| [JAR](https://search.maven.org/remotecontent?filepath=org/primefaces/primefaces/13.0.10/primefaces-13.0.10.jar)  | [Sources](https://search.maven.org/remotecontent?filepath=org/primefaces/primefaces/13.0.10/primefaces-13.0.10-sources.jar) | 2.0 - 4.0 | 8 - ? | [13.0.10 Documentation](https://primefaces.github.io/primefaces/13_0_0/#/)
 ![12.0.x](https://img.shields.io/maven-central/v/org.primefaces/primefaces.svg?versionPrefix=12&color=cyan)| [JAR](https://search.maven.org/remotecontent?filepath=org/primefaces/primefaces/12.0.0/primefaces-12.0.0.jar)  | [Sources](https://search.maven.org/remotecontent?filepath=org/primefaces/primefaces/12.0.0/primefaces-12.0.0-sources.jar) | 2.0 - 4.0 | 8 - ? | [12.0.0 Documentation](https://primefaces.github.io/primefaces/12_0_0/#/)
 
 <details>
diff --git a/docs/15_0_0/components/feedreader.md b/docs/15_0_0/components/feedreader.md
index 5f60953224..3acf238ffb 100644
--- a/docs/15_0_0/components/feedreader.md
+++ b/docs/15_0_0/components/feedreader.md
@@ -23,19 +23,21 @@ FeedReader is used to display content from a feed.
 | value | null | String | URL of the feed.
 | var | null | String | Iterator to refer each item in feed.
 | size | unlimited | Integer | Number of items to display.
+| podcast | false | Boolean | Flag to control if this is a podcast which will include extra Podcast metadata.
 
 ## Getting started with FeedReader
 
-FeedReader requires a feed url to display and renders its content for each feed item.
+FeedReader requires a feed URL to display and renders its content for each feed item.
 
 ```xhtml
 <p:feedReader value="http://rss.news.yahoo.com/rss/sports" var="feed">
     <h:outputText value="#{feed.title}" style="font-weight: bold"/>
-    <h:outputText value="#{feed.description.value}" escape="false"/>
+    <h:outputText value="#{feed.description}" escape="false"/>
     <p:separator />
     <f:facet name="error">
         Something went wrong.
     </f:facet>
 </p:feedReader>
 ```
-**Note** that you need the ROME library in your classpath to make feedreader work.
\ No newline at end of file
+
+!> NOTE: You need the [RSS Reader](https://github.com/w3stling/rssreader) library in your classpath to make FeedReader work.
\ No newline at end of file
diff --git a/docs/15_0_0/gettingstarted/dependencies.md b/docs/15_0_0/gettingstarted/dependencies.md
index feb96f52c2..d9466b20d2 100644
--- a/docs/15_0_0/gettingstarted/dependencies.md
+++ b/docs/15_0_0/gettingstarted/dependencies.md
@@ -13,7 +13,7 @@ any 3rd part work incorporated are compatible with the PrimeFaces Licenses.
 | Libre OpenPDF | com.github.librepdf | openpdf | 1.4.1 | Optional | DataExporter (PDF) |
 | Apache POI | org.apache.poi | poi | 5.2.5 | Optional | DataExporter (Excel XLS or XML) |
 | Apache POI | org.apache.poi | poi-ooxml | 5.2.5 | Optional | DataExporter (Excel XLSX Office 2003) |
-| Rome | com.rometools | rome | 1.15.0 | Optional | FeedReader |
+| RSS Reader | com.apptasticsoftware | rssreader | 3.6.0 | Optional | FeedReader |
 | barcode4j-light | org.primefaces.extensions | barcode4j-light | 2.3.0 | Optional | Barcode |
 | qrcodegen | io.nayuki | qrcodegen | 1.8.0 | Optional | QR Code support for Barcode |
 | owasp-java-html-sanitizer | com.googlecode.owasp-java-html-sanitizer | owasp-java-html-sanitizer |  20240325.1 | Optional | TextEditor |
diff --git a/docs/15_0_0/gettingstarted/whatsnew.md b/docs/15_0_0/gettingstarted/whatsnew.md
index d50bc9ad62..6f14971a2c 100644
--- a/docs/15_0_0/gettingstarted/whatsnew.md
+++ b/docs/15_0_0/gettingstarted/whatsnew.md
@@ -13,4 +13,6 @@ Look into [migration guide](https://primefaces.github.io/primefaces/15_0_0/#/../
 
 * Captcha
     * Added [hCaptcha](https://www.hcaptcha.com/) support
-    
\ No newline at end of file
+    
+* FeedReader
+    * Added `podcast="true"` property if [Apple Itunes Podcast](https://help.apple.com/itc/podcasts_connect/#/itcb54353390) parsing and specific tags 
\ No newline at end of file
diff --git a/docs/migrationguide/15_0_0.md b/docs/migrationguide/15_0_0.md
index c6ba6be681..1fd5f3ad46 100644
--- a/docs/migrationguide/15_0_0.md
+++ b/docs/migrationguide/15_0_0.md
@@ -16,3 +16,6 @@
 
   * 
 
+## FeedReader
+
+  * Switched from [Rome](https://rometools.github.io/rome/) library which uses XML parsing to non XML very fast [RSS Reader](https://github.com/w3stling/rssreader)
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 5a090862b4..cecf27cd08 100644
--- a/pom.xml
+++ b/pom.xml
@@ -363,9 +363,9 @@
 
             <!-- FeedReader -->
             <dependency>
-                <groupId>com.rometools</groupId>
-                <artifactId>rome</artifactId>
-                <version>1.19.0</version>
+                <groupId>com.apptasticsoftware</groupId>
+                <artifactId>rssreader</artifactId>
+                <version>3.6.0</version>
             </dependency>
 
             <!-- File Upload -->
diff --git a/primefaces-showcase/pom.xml b/primefaces-showcase/pom.xml
index 1a0ae08ab5..2ac9c71da5 100644
--- a/primefaces-showcase/pom.xml
+++ b/primefaces-showcase/pom.xml
@@ -96,8 +96,8 @@
 
         <!-- FeedReader -->
         <dependency>
-            <groupId>com.rometools</groupId>
-            <artifactId>rome</artifactId>
+            <groupId>com.apptasticsoftware</groupId>
+            <artifactId>rssreader</artifactId>
         </dependency>
 
         <!-- File Upload -->
diff --git a/primefaces-showcase/src/main/webapp/ui/misc/feedReader.xhtml b/primefaces-showcase/src/main/webapp/ui/misc/feedReader.xhtml
index bc4c850a05..9c98217f75 100644
--- a/primefaces-showcase/src/main/webapp/ui/misc/feedReader.xhtml
+++ b/primefaces-showcase/src/main/webapp/ui/misc/feedReader.xhtml
@@ -10,7 +10,7 @@
     </ui:define>
 
     <ui:define name="description">
-        FeedReader retrieves and displays content from a feed.
+        FeedReader retrieves and displays content from an RSS, Atom, or Itunes Podcast feed.
     </ui:define>
 
     <ui:param name="documentationLink" value="/components/feedreader"/>
@@ -20,7 +20,7 @@
             <p:feedReader value="https://www.nasa.gov/rss/dyn/breaking_news.rss" var="feed">
                 <div class="p-3">
                     <h:outputText value="#{feed.title}" styleClass="font-bold block" />
-                    <h:outputText value="#{feed.description.value}" escape="false"/>
+                    <h:outputText value="#{feed.description}" escape="false"/>
                     <p:divider/>
                 </div>
                 
diff --git a/primefaces/pom.xml b/primefaces/pom.xml
index 77f88a1a4f..85a23eab2d 100644
--- a/primefaces/pom.xml
+++ b/primefaces/pom.xml
@@ -119,15 +119,9 @@
 
         <!-- FeedReader -->
         <dependency>
-            <groupId>com.rometools</groupId>
-            <artifactId>rome</artifactId>
+            <groupId>com.apptasticsoftware</groupId>
+            <artifactId>rssreader</artifactId>
             <scope>provided</scope>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.slf4j</groupId>
-                    <artifactId>slf4j-api</artifactId>
-                </exclusion>
-            </exclusions>
         </dependency>
 
         <!-- JAXB for JDK11+-->
diff --git a/primefaces/src/main/java/org/primefaces/component/feedreader/FeedInput.java b/primefaces/src/main/java/org/primefaces/component/feedreader/FeedInput.java
deleted file mode 100644
index cdda6ee7ea..0000000000
--- a/primefaces/src/main/java/org/primefaces/component/feedreader/FeedInput.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * The MIT License
- *
- * Copyright (c) 2009-2024 PrimeTek Informatics
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-package org.primefaces.component.feedreader;
-
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.List;
-
-import com.rometools.rome.feed.synd.SyndFeed;
-import com.rometools.rome.io.FeedException;
-import com.rometools.rome.io.SyndFeedInput;
-import com.rometools.rome.io.XmlReader;
-import java.io.IOException;
-
-public class FeedInput {
-
-    public List parse(String url, int size) throws IOException, IllegalArgumentException, FeedException {
-        List entries = new ArrayList();
-        URL feedSource = new URL(url);
-        SyndFeedInput input = new SyndFeedInput();
-        SyndFeed feed = input.build(new XmlReader(feedSource));
-        int i = 0;
-
-        for (Object f : feed.getEntries()) {
-            if (i == size) {
-                break;
-            }
-
-            entries.add(f);
-            i++;
-        }
-
-        return entries;
-    }
-}
diff --git a/primefaces/src/main/java/org/primefaces/component/feedreader/FeedReaderBase.java b/primefaces/src/main/java/org/primefaces/component/feedreader/FeedReaderBase.java
index 5c5f9944d5..bf284e2004 100644
--- a/primefaces/src/main/java/org/primefaces/component/feedreader/FeedReaderBase.java
+++ b/primefaces/src/main/java/org/primefaces/component/feedreader/FeedReaderBase.java
@@ -36,7 +36,8 @@ public abstract class FeedReaderBase extends UIComponentBase {
 
         value,
         var,
-        size
+        size,
+        podcast
     }
 
     public FeedReaderBase() {
@@ -72,4 +73,12 @@ public abstract class FeedReaderBase extends UIComponentBase {
         getStateHelper().put(PropertyKeys.size, size);
     }
 
+    public boolean isPodcast() {
+        return (Boolean) getStateHelper().eval(PropertyKeys.podcast, Boolean.FALSE);
+    }
+
+    public void setPodcast(Boolean podcast) {
+        getStateHelper().put(PropertyKeys.podcast, podcast);
+    }
+
 }
\ No newline at end of file
diff --git a/primefaces/src/main/java/org/primefaces/component/feedreader/FeedReaderRenderer.java b/primefaces/src/main/java/org/primefaces/component/feedreader/FeedReaderRenderer.java
index 50d132344c..84d3b2ab4b 100644
--- a/primefaces/src/main/java/org/primefaces/component/feedreader/FeedReaderRenderer.java
+++ b/primefaces/src/main/java/org/primefaces/component/feedreader/FeedReaderRenderer.java
@@ -30,6 +30,7 @@ import java.util.Map;
 import javax.faces.component.UIComponent;
 import javax.faces.context.FacesContext;
 
+import org.primefaces.model.feedreader.FeedItem;
 import org.primefaces.renderkit.CoreRenderer;
 import org.primefaces.util.FacetUtils;
 
@@ -38,15 +39,16 @@ public class FeedReaderRenderer extends CoreRenderer {
     @Override
     public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
         FeedReader reader = (FeedReader) component;
-        Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
-        String var = reader.getVar();
-        int size = reader.getSize();
 
         try {
-            List entries = new FeedInput().parse(reader.getValue(), size);
+            Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
+            String var = reader.getVar();
+            int size = reader.getSize();
+            String url = reader.getValue();
+            List<FeedItem> entries = RSSUtils.parse(url, size, reader.isPodcast());
 
-            for (Object f : entries) {
-                requestMap.put(var, f);
+            for (FeedItem item : entries) {
+                requestMap.put(var, item);
                 renderChildren(context, reader);
             }
 
@@ -54,6 +56,7 @@ public class FeedReaderRenderer extends CoreRenderer {
 
         }
         catch (Exception e) {
+            logDevelopmentWarning(context, this, String.format("Unexpected RSS error: %s", e.getMessage()));
             UIComponent errorFacet = reader.getFacet("error");
             if (FacetUtils.shouldRenderFacet(errorFacet)) {
                 errorFacet.encodeAll(context);
diff --git a/primefaces/src/main/java/org/primefaces/component/feedreader/RSSUtils.java b/primefaces/src/main/java/org/primefaces/component/feedreader/RSSUtils.java
new file mode 100644
index 0000000000..0d995bd597
--- /dev/null
+++ b/primefaces/src/main/java/org/primefaces/component/feedreader/RSSUtils.java
@@ -0,0 +1,111 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2024 PrimeTek Informatics
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.component.feedreader;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.primefaces.model.feedreader.FeedItem;
+import org.primefaces.util.Constants;
+
+import com.apptasticsoftware.rssreader.AbstractRssReader;
+import com.apptasticsoftware.rssreader.Item;
+import com.apptasticsoftware.rssreader.RssReader;
+import com.apptasticsoftware.rssreader.module.itunes.ItunesItem;
+import com.apptasticsoftware.rssreader.module.itunes.ItunesRssReader;
+
+/**
+ * Static utility class to parse feed input into a list of FeedItem objects.
+ */
+public class RSSUtils {
+
+    private RSSUtils() {
+        // Private constructor to prevent instantiation
+    }
+
+    /**
+     * Parses the input into a list of FeedItem objects.
+     *
+     * @param input     The input source, either a String representing the URL of the feed or an InputStream.
+     * @param size      The maximum number of items to retrieve from the feed.
+     * @param isPodcast Specifies whether the feed is a podcast feed.
+     * @return A list of FeedItem objects parsed from the input.
+     * @throws IOException If an I/O error occurs during the parsing process.
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    public static List<FeedItem> parse(Object input, int size, boolean isPodcast) throws IOException {
+        AbstractRssReader reader;
+        if (isPodcast) {
+            reader = new ItunesRssReader();
+        }
+        else {
+            reader = new RssReader();
+        }
+        if (input instanceof String) {
+            return convert((List<Item>) reader.read((String) input).limit(size).collect(Collectors.toList()));
+        }
+        return convert((List<Item>) reader.read((InputStream) input).limit(size).collect(Collectors.toList()));
+    }
+
+    /**
+     * Converts a list of generic RSS items into a list of FeedItem objects.
+     *
+     * @param items The list of generic RSS items to convert.
+     * @return A list of FeedItem objects converted from the input list of items.
+     */
+    private static List<FeedItem> convert(List<Item> items) {
+        List<FeedItem> results = new ArrayList<>(items.size());
+        for (Item item : items) {
+            FeedItem fi = new FeedItem();
+            fi.setAuthor(item.getAuthor().orElse(Constants.EMPTY_STRING));
+            fi.getCategories().addAll(item.getCategories());
+            fi.setComments(item.getComments().orElse(Constants.EMPTY_STRING));
+            fi.setDescription(item.getDescription().orElse(Constants.EMPTY_STRING));
+            fi.setGuid(item.getGuid().orElse(Constants.EMPTY_STRING));
+            fi.setIsPermaLink(item.getIsPermaLink().orElse(Boolean.FALSE));
+            fi.setLink(item.getLink().orElse(Constants.EMPTY_STRING));
+            fi.setPubDate(item.getPubDate().orElse(Constants.EMPTY_STRING));
+            fi.setTitle(item.getTitle().orElse(Constants.EMPTY_STRING));
+            if (item instanceof ItunesItem) {
+                ItunesItem itunes = (ItunesItem) item;
+                fi.setItunesBlock(itunes.isItunesBlock());
+                fi.setItunesDuration(itunes.getItunesDuration().orElse(Constants.EMPTY_STRING));
+                fi.setItunesEpisode(itunes.getItunesEpisode().orElse(0));
+                fi.setItunesEpisodeType(itunes.getItunesEpisodeType().orElse(Constants.EMPTY_STRING));
+                fi.setItunesExplicit(itunes.isItunesExplicit());
+                fi.setItunesImage(itunes.getItunesImage().orElse(Constants.EMPTY_STRING));
+                fi.setItunesKeywords(itunes.getItunesKeywords().orElse(Constants.EMPTY_STRING));
+                fi.setItunesSeason(itunes.getItunesSeason().orElse(0));
+                fi.setItunesSubtitle(itunes.getItunesSubtitle().orElse(Constants.EMPTY_STRING));
+                fi.setItunesSummary(itunes.getItunesSummary().orElse(Constants.EMPTY_STRING));
+                fi.setItunesTitle(itunes.getItunesTitle().orElse(Constants.EMPTY_STRING));
+            }
+            results.add(fi);
+        }
+        return results;
+    }
+}
diff --git a/primefaces/src/main/java/org/primefaces/model/feedreader/FeedItem.java b/primefaces/src/main/java/org/primefaces/model/feedreader/FeedItem.java
new file mode 100644
index 0000000000..bb56d2cf3f
--- /dev/null
+++ b/primefaces/src/main/java/org/primefaces/model/feedreader/FeedItem.java
@@ -0,0 +1,260 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2024 PrimeTek Informatics
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.model.feedreader;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Class representing a RSS item. A channel may contain any number of items. An item may represent a "story" -- much
+ * like a story in a newspaper or magazine; if so its description is a synopsis of the story, and the link points
+ * to the full story.
+ */
+public class FeedItem implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private String title;
+    private String description;
+    private String link;
+    private String author;
+    private final List<String> categories = new ArrayList<>();
+    private String guid;
+    private Boolean isPermaLink;
+    private String pubDate;
+    private String comments;
+    private String itunesDuration;
+    private String itunesImage;
+    private boolean itunesExplicit;
+    private String itunesTitle;
+    private String itunesSubtitle;
+    private String itunesSummary;
+    private String itunesKeywords;
+    private Integer itunesEpisode;
+    private Integer itunesSeason;
+    private String itunesEpisodeType;
+    private boolean itunesBlock;
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public String getLink() {
+        return link;
+    }
+
+    public void setLink(String link) {
+        this.link = link;
+    }
+
+    public String getAuthor() {
+        return author;
+    }
+
+    public void setAuthor(String author) {
+        this.author = author;
+    }
+
+    public String getGuid() {
+        return guid;
+    }
+
+    public void setGuid(String guid) {
+        this.guid = guid;
+    }
+
+    public Boolean getIsPermaLink() {
+        return isPermaLink;
+    }
+
+    public void setIsPermaLink(Boolean isPermaLink) {
+        this.isPermaLink = isPermaLink;
+    }
+
+    public String getPubDate() {
+        return pubDate;
+    }
+
+    public void setPubDate(String pubDate) {
+        this.pubDate = pubDate;
+    }
+
+    public String getComments() {
+        return comments;
+    }
+
+    public void setComments(String comments) {
+        this.comments = comments;
+    }
+
+    public String getItunesDuration() {
+        return itunesDuration;
+    }
+
+    public void setItunesDuration(String itunesDuration) {
+        this.itunesDuration = itunesDuration;
+    }
+
+    public String getItunesImage() {
+        return itunesImage;
+    }
+
+    public void setItunesImage(String itunesImage) {
+        this.itunesImage = itunesImage;
+    }
+
+    public boolean isItunesExplicit() {
+        return itunesExplicit;
+    }
+
+    public void setItunesExplicit(boolean itunesExplicit) {
+        this.itunesExplicit = itunesExplicit;
+    }
+
+    public String getItunesTitle() {
+        return itunesTitle;
+    }
+
+    public void setItunesTitle(String itunesTitle) {
+        this.itunesTitle = itunesTitle;
+    }
+
+    public String getItunesSubtitle() {
+        return itunesSubtitle;
+    }
+
+    public void setItunesSubtitle(String itunesSubtitle) {
+        this.itunesSubtitle = itunesSubtitle;
+    }
+
+    public String getItunesSummary() {
+        return itunesSummary;
+    }
+
+    public void setItunesSummary(String itunesSummary) {
+        this.itunesSummary = itunesSummary;
+    }
+
+    public String getItunesKeywords() {
+        return itunesKeywords;
+    }
+
+    public void setItunesKeywords(String itunesKeywords) {
+        this.itunesKeywords = itunesKeywords;
+    }
+
+    public Integer getItunesEpisode() {
+        return itunesEpisode;
+    }
+
+    public void setItunesEpisode(Integer itunesEpisode) {
+        this.itunesEpisode = itunesEpisode;
+    }
+
+    public Integer getItunesSeason() {
+        return itunesSeason;
+    }
+
+    public void setItunesSeason(Integer itunesSeason) {
+        this.itunesSeason = itunesSeason;
+    }
+
+    public String getItunesEpisodeType() {
+        return itunesEpisodeType;
+    }
+
+    public void setItunesEpisodeType(String itunesEpisodeType) {
+        this.itunesEpisodeType = itunesEpisodeType;
+    }
+
+    public boolean isItunesBlock() {
+        return itunesBlock;
+    }
+
+    public void setItunesBlock(boolean itunesBlock) {
+        this.itunesBlock = itunesBlock;
+    }
+
+    public List<String> getCategories() {
+        return categories;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(author, categories, comments, description, guid, isPermaLink, itunesBlock, itunesDuration, itunesEpisode,
+                itunesEpisodeType, itunesExplicit, itunesImage, itunesKeywords, itunesSeason, itunesSubtitle, itunesSummary, itunesTitle, link, pubDate, title);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        FeedItem other = (FeedItem) obj;
+        return Objects.equals(author, other.author) && Objects.equals(categories, other.categories)
+                && Objects.equals(comments, other.comments) && Objects.equals(description, other.description) && Objects.equals(guid, other.guid)
+                && Objects.equals(isPermaLink, other.isPermaLink) && itunesBlock == other.itunesBlock && Objects.equals(itunesDuration, other.itunesDuration)
+                && Objects.equals(itunesEpisode, other.itunesEpisode) && Objects.equals(itunesEpisodeType, other.itunesEpisodeType)
+                && itunesExplicit == other.itunesExplicit && Objects.equals(itunesImage, other.itunesImage)
+                && Objects.equals(itunesKeywords, other.itunesKeywords) && Objects.equals(itunesSeason, other.itunesSeason)
+                && Objects.equals(itunesSubtitle, other.itunesSubtitle) && Objects.equals(itunesSummary, other.itunesSummary)
+                && Objects.equals(itunesTitle, other.itunesTitle) && Objects.equals(link, other.link) && Objects.equals(pubDate, other.pubDate)
+                && Objects.equals(title, other.title);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("FeedItem [title=").append(title).append(", description=").append(description).append(", link=").append(link).append(", author=")
+                .append(author).append(", categories=").append(categories).append(", guid=").append(guid)
+                .append(", isPermaLink=").append(isPermaLink).append(", pubDate=").append(pubDate).append(", comments=").append(comments)
+                .append(", itunesDuration=").append(itunesDuration).append(", itunesImage=").append(itunesImage).append(", itunesExplicit=")
+                .append(itunesExplicit).append(", itunesTitle=").append(itunesTitle).append(", itunesSubtitle=").append(itunesSubtitle)
+                .append(", itunesSummary=").append(itunesSummary).append(", itunesKeywords=").append(itunesKeywords).append(", itunesEpisode=")
+                .append(itunesEpisode).append(", itunesSeason=").append(itunesSeason).append(", itunesEpisodeType=").append(itunesEpisodeType)
+                .append(", itunesBlock=").append(itunesBlock).append("]");
+        return builder.toString();
+    }
+
+}
diff --git a/primefaces/src/main/resources/META-INF/primefaces-p.taglib.xml b/primefaces/src/main/resources/META-INF/primefaces-p.taglib.xml
index 1e6a5ca14a..8509248a2a 100644
--- a/primefaces/src/main/resources/META-INF/primefaces-p.taglib.xml
+++ b/primefaces/src/main/resources/META-INF/primefaces-p.taglib.xml
@@ -10436,7 +10436,7 @@
     </tag>
     <tag>
         <description>
-            <![CDATA[feedReader displays content from a feed.]]>
+            <![CDATA[FeedReader retrieves and displays content from an RSS, Atom, or Itunes Podcast feed.]]>
         </description>
         <tag-name>feedReader</tag-name>
         <component>
@@ -10491,6 +10491,14 @@
             <required>false</required>
             <type>java.lang.Integer</type>
         </attribute>
+        <attribute>
+            <description>
+                <![CDATA[Flag to control if this is a podcast feed which will include extra podcast metadata. Default false.]]>
+            </description>
+            <name>podcast</name>
+            <required>false</required>
+            <type>java.lang.Boolean</type>
+        </attribute>
     </tag>
     <tag>
         <description>


Test patch:
diff --git a/primefaces-integration-tests/pom.xml b/primefaces-integration-tests/pom.xml
index 3f719b6468..db641f6bc8 100644
--- a/primefaces-integration-tests/pom.xml
+++ b/primefaces-integration-tests/pom.xml
@@ -48,8 +48,8 @@
 
         <!-- FeedReader -->
         <dependency>
-            <groupId>com.rometools</groupId>
-            <artifactId>rome</artifactId>
+            <groupId>com.apptasticsoftware</groupId>
+            <artifactId>rssreader</artifactId>
             <scope>runtime</scope>
         </dependency>
         
diff --git a/primefaces-integration-tests/src/main/webapp/feedreader/feedReader001.xhtml b/primefaces-integration-tests/src/main/webapp/feedreader/feedReader001.xhtml
index c10bf7a189..bc72baa07d 100644
--- a/primefaces-integration-tests/src/main/webapp/feedreader/feedReader001.xhtml
+++ b/primefaces-integration-tests/src/main/webapp/feedreader/feedReader001.xhtml
@@ -17,7 +17,7 @@
             <p:feedReader value="#{feedReader001.current}" var="feed" id="feedReader">
                 <div class="p-p-3">
                     <h:outputText value="#{feed.title}" styleClass="p-text-bold p-d-block ui-rss-feed" />
-                    <h:outputText value="#{feed.description.value}" escape="false"/>
+                    <h:outputText value="#{feed.description}" escape="false"/>
                 </div>
 
                 <f:facet name="error">
diff --git a/primefaces/src/test/java/org/primefaces/component/feedreader/FeedReaderTest.java b/primefaces/src/test/java/org/primefaces/component/feedreader/RSSUtilsTest.java
similarity index 61%
rename from primefaces/src/test/java/org/primefaces/component/feedreader/FeedReaderTest.java
rename to primefaces/src/test/java/org/primefaces/component/feedreader/RSSUtilsTest.java
index d7749f3428..2b8034c72d 100644
--- a/primefaces/src/test/java/org/primefaces/component/feedreader/FeedReaderTest.java
+++ b/primefaces/src/test/java/org/primefaces/component/feedreader/RSSUtilsTest.java
@@ -23,34 +23,34 @@
  */
 package org.primefaces.component.feedreader;
 
-import com.rometools.rome.io.ParsingFeedException;
 import java.net.URL;
 import java.util.List;
 import org.junit.jupiter.api.Test;
+import org.primefaces.model.feedreader.FeedItem;
+
 import static org.junit.jupiter.api.Assertions.*;
 
-class FeedReaderTest {
+class RSSUtilsTest {
 
     @Test()
-    void parseXXEbomb() throws Exception {
-        // Check CVE-2021-33813 can not be triggered in Primefaces
-        // See https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-33813
-        // See https://alephsecurity.com/vulns/aleph-2021003
-        URL feed = FeedReaderTest.class.getResource("/org/primefaces/feeds/XXEbomb.xml");
-        assertThrows(ParsingFeedException.class, () -> new FeedInput().parse(feed.toString(), 1));
-    }
-
-    @Test()
-    void parseXXE() throws Exception {
-        URL feed = FeedReaderTest.class.getResource("/org/primefaces/feeds/XXE.xml");
-        assertThrows(ParsingFeedException.class, () -> new FeedInput().parse(feed.toString(), 1));
+    void parseRSS() throws Exception {
+        URL feed = RSSUtilsTest.class.getResource("/org/primefaces/feeds/RSS2.0.xml");
+        List<FeedItem> rss = RSSUtils.parse(feed.openStream(), 10, false);
+        assertNotNull(rss);
+        assertEquals(2, rss.size());
+        assertEquals("News for September the Second", rss.get(0).getTitle());
+        assertEquals("http://example.com/2002/09/02", rss.get(1).getLink());
     }
 
     @Test()
-    void parseRSS() throws Exception {
-        URL feed = FeedReaderTest.class.getResource("/org/primefaces/feeds/RSS2.0.xml");
-        List rss = new FeedInput().parse(feed.toString(), 10);
+    void parsePodcast() throws Exception {
+        URL feed = RSSUtilsTest.class.getResource("/org/primefaces/feeds/podcast.xml");
+        List<FeedItem> rss = RSSUtils.parse(feed.openStream(), 5, true);
         assertNotNull(rss);
-        assertEquals(2, rss.size());
+        assertEquals(5, rss.size());
+        assertEquals("Hiking Treks Trailer", rss.get(0).getItunesTitle());
+        assertEquals("1079", rss.get(0).getItunesDuration());
+        assertEquals("S02 EP04 Mt. Hood, Oregon", rss.get(1).getTitle());
+        assertEquals("1024", rss.get(1).getItunesDuration());
     }
 }
diff --git a/primefaces/src/test/resources/org/primefaces/feeds/XXE.xml b/primefaces/src/test/resources/org/primefaces/feeds/XXE.xml
deleted file mode 100644
index fa34bf6dfb..0000000000
--- a/primefaces/src/test/resources/org/primefaces/feeds/XXE.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="ISO-8859-1"?>
-<!DOCTYPE rss [
-   <!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
-<rss version="2.0">
-  <channel>
-    <title>&xxe;</title>
-  </channel>
-</rss>
diff --git a/primefaces/src/test/resources/org/primefaces/feeds/XXEbomb.xml b/primefaces/src/test/resources/org/primefaces/feeds/XXEbomb.xml
deleted file mode 100644
index 451666c90e..0000000000
--- a/primefaces/src/test/resources/org/primefaces/feeds/XXEbomb.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<!DOCTYPE foo [<!ELEMENT foo ANY><!ENTITY bar "World ">
-	<!ENTITY t1 "&bar;&bar;"><!ENTITY t2 "&t1;&t1;&t1;&t1;">
-	<!ENTITY t3 "&t2;&t2;&t2;&t2;&t2;">]>
-<foo>Hello &t3;</foo>
diff --git a/primefaces/src/test/resources/org/primefaces/feeds/podcast.xml b/primefaces/src/test/resources/org/primefaces/feeds/podcast.xml
new file mode 100644
index 0000000000..e9561276b2
--- /dev/null
+++ b/primefaces/src/test/resources/org/primefaces/feeds/podcast.xml
@@ -0,0 +1,194 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/">
+  <channel>
+    <title>Hiking Treks</title>
+    <link>https://www.apple.com/itunes/podcasts/</link>
+    <language>en-us</language>
+    <copyright>&#169; 2020 John Appleseed</copyright>
+    <itunes:author>The Sunset Explorers</itunes:author>
+    <description>
+      Love to get outdoors and discover nature&apos;s treasures? Hiking Treks is the
+      show for you. We review hikes and excursions, review outdoor gear and interview
+      a variety of naturalists and adventurers. Look for new episodes each week.
+    </description>
+    <itunes:type>serial</itunes:type>
+    <itunes:image
+      href="https://applehosted.podcasts.apple.com/hiking_treks/artwork.png"
+    />
+    <itunes:category text="Sports">
+      <itunes:category text="Wilderness"/>
+    </itunes:category>
+    <itunes:explicit>false</itunes:explicit>
+    <item>
+      <itunes:episodeType>trailer</itunes:episodeType>
+      <itunes:title>Hiking Treks Trailer</itunes:title>
+      <description>
+          <![CDATA[The Sunset Explorers share tips, techniques and recommendations for
+          great hikes and adventures around the United States. Listen on 
+          <a href="https://www.apple.com/itunes/podcasts/">Apple Podcasts</a>.]]>
+      </description>
+      <enclosure 
+        length="498537" 
+        type="audio/mpeg" 
+        url="http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3"
+      />
+      <guid>D03EEC9B-B1B4-475B-92C8-54F853FA2A22</guid>
+      <pubDate>Tue, 8 Jan 2019 01:15:00 GMT</pubDate>
+      <itunes:duration>1079</itunes:duration>
+      <itunes:explicit>false</itunes:explicit>
+    </item>
+    <item>
+      <itunes:episodeType>full</itunes:episodeType>
+      <itunes:episode>4</itunes:episode>
+      <itunes:season>2</itunes:season>
+      <title>S02 EP04 Mt. Hood, Oregon</title>
+      <description>
+        Tips for trekking around the tallest mountain in Oregon
+      </description>
+      <enclosure
+        length="8727310" 
+        type="audio/x-m4a" 
+        url="http://example.com/podcasts/everything/mthood.m4a"
+      />
+      <guid>22BCFEBF-44FB-4A19-8229-7AC678629F57</guid>
+      <pubDate>Tue, 07 May 2019 12:00:00 GMT</pubDate>
+      <itunes:duration>1024</itunes:duration>
+      <itunes:explicit>false</itunes:explicit>
+    </item>
+    <item>
+      <itunes:episodeType>full</itunes:episodeType>
+      <itunes:episode>3</itunes:episode>
+      <itunes:season>2</itunes:season>
+      <title>S02 EP03 Bouldering Around Boulder</title>
+      <description>
+        We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.
+      </description>
+      <itunes:image
+        href="http://example.com/podcasts/everything/AllAboutEverything/Episode2.jpg"
+      />
+      <link>href="http://example.com/podcasts/everything/</link>
+      <enclosure 
+        length="5650889" 
+        type="video/mp4" 
+        url="http://example.com/podcasts/boulder.mp4"
+      />
+      <guid>BE486CAA-B3D5-4FB0-8298-EFEBE71C5982</guid>
+      <pubDate>Tue, 30 Apr 2019 13:00:00 EST</pubDate>
+      <itunes:duration>3627</itunes:duration>
+      <itunes:explicit>false</itunes:explicit>
+    </item>
+    <item>
+      <itunes:episodeType>full</itunes:episodeType>
+      <itunes:episode>2</itunes:episode>
+      <itunes:season>2</itunes:season>
+      <title>S02 EP02 Caribou Mountain, Maine</title>
+      <description>
+        Put your fitness to the test with this invigorating hill climb.
+      </description>
+      <itunes:image
+        href="http://example.com/podcasts/everything/AllAboutEverything/Episode3.jpg"
+      />
+      <enclosure 
+        length="5650889"
+        type="audio/x-m4v" 
+        url="http://example.com/podcasts/everything/caribou.m4v"
+      />
+      <guid>142FAFE9-B1DF-4F6D-BAA8-79BDBAF653A9</guid>
+      <pubDate>Tue, 23 May 2019 02:00:00 -0700</pubDate>
+      <itunes:duration>2434</itunes:duration>
+      <itunes:explicit>false</itunes:explicit>
+    </item>
+    <item>
+      <itunes:episodeType>full</itunes:episodeType>
+      <itunes:episode>1</itunes:episode>
+      <itunes:season>2</itunes:season>
+      <title>S02 EP01 Stawamus Chief</title>
+      <description>
+        We tackle Stawamus Chief outside of Vancouver, BC and you should too!
+      </description>
+      <enclosure
+        length="498537" 
+        type="audio/mpeg" 
+        url="http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3"
+      />
+      <guid>5F1DBAEB-3327-49FB-ACB3-DB0158A1D0A3</guid>
+      <pubDate>2019-02-16T07:00:00.000Z</pubDate>
+      <itunes:duration>13:24</itunes:duration>
+      <itunes:explicit>false</itunes:explicit>
+    </item>
+    <item>
+      <itunes:episodeType>full</itunes:episodeType>
+      <itunes:episode>4</itunes:episode>
+      <itunes:season>1</itunes:season>
+      <title>S01 EP04 Kuliouou Ridge Trail</title>
+      <description>
+        Oahu, Hawaii, has some picturesque hikes and this is one of the best!
+      </description>
+      <enclosure
+        length="498537" 
+        type="audio/mpeg" 
+        url="http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3"
+      />
+      <guid>B5FCEB80-317C-4CD0-A84B-807065B43FB9</guid>
+      <pubDate>Tue, 27 Nov 2018 01:15:00 +0000</pubDate>
+      <itunes:duration>929</itunes:duration>
+      <itunes:explicit>false</itunes:explicit>
+    </item>
+    <item>
+      <itunes:episodeType>full</itunes:episodeType>
+      <itunes:episode>3</itunes:episode>
+      <itunes:season>1</itunes:season>
+      <title>S01 EP03 Blood Mountain Loop</title>
+      <description>
+        Hiking the Appalachian Trail and Freeman Trail in Georgia
+      </description>
+      <enclosure 
+        length="498537" 
+        type="audio/mpeg" 
+        url="http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3"
+      />
+      <guid>F0C5D763-ED85-4449-9C09-81FEBDF6F126</guid>
+      <pubDate>Tue, 23 Oct 2018 01:15:00 +0000</pubDate>
+      <itunes:duration>1440</itunes:duration>
+      <itunes:explicit>false</itunes:explicit>
+    </item>
+    <item>
+      <itunes:episodeType>full</itunes:episodeType>
+      <itunes:episode>2</itunes:episode>
+      <itunes:season>1</itunes:season>
+      <title>S01 EP02 Garden of the Gods Wilderness</title>
+      <description>
+        Wilderness Area Garden of the Gods in Illinois is a delightful spot for 
+        an extended hike.
+      </description>
+      <enclosure 
+        length="498537" 
+        type="audio/mpeg" 
+        url="http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3"
+      />
+      <guid>821DD0B2-571D-4DFD-8E11-556E8C1EFE6A</guid>
+      <pubDate>Tue, 18 Sep 2018 01:15:00 +0000</pubDate>
+      <itunes:duration>839</itunes:duration>
+      <itunes:explicit>false</itunes:explicit>
+    </item>
+    <item>
+      <itunes:episodeType>full</itunes:episodeType>
+      <itunes:episode>1</itunes:episode>
+      <itunes:season>1</itunes:season>
+      <title>S01 EP01 Upper Priest Lake Trail to Continental Creek Trail</title>
+      <description>
+        We check out this powerfully scenic hike following the river in the Idaho
+        Panhandle National Forests.
+      </description>
+      <enclosure 
+        length="498537" 
+        type="audio/mpeg" 
+        url="http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3"
+      />
+      <guid>EABDA7EE-1AC6-4B60-9E11-6B3F30B72F87</guid>
+      <pubDate>Tue, 14 Aug 2018 01:15:00 +0000</pubDate>
+      <itunes:duration>1399</itunes:duration>
+      <itunes:explicit>false</itunes:explicit>
+    </item>
+  </channel>
+</rss>
\ No newline at end of file