File: System\ServiceModel\Syndication\Rss20FeedFormatter.cs
Web Access
Project: src\src\libraries\System.ServiceModel.Syndication\src\System.ServiceModel.Syndication.csproj (System.ServiceModel.Syndication)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
 
namespace System.ServiceModel.Syndication
{
    [XmlRoot(ElementName = Rss20Constants.RssTag, Namespace = Rss20Constants.Rss20Namespace)]
    public class Rss20FeedFormatter : SyndicationFeedFormatter, IXmlSerializable
    {
        private static readonly XmlQualifiedName s_rss20Domain = new XmlQualifiedName(Rss20Constants.DomainTag, string.Empty);
        private static readonly XmlQualifiedName s_rss20Length = new XmlQualifiedName(Rss20Constants.LengthTag, string.Empty);
        private static readonly XmlQualifiedName s_rss20Type = new XmlQualifiedName(Rss20Constants.TypeTag, string.Empty);
        private static readonly XmlQualifiedName s_rss20Url = new XmlQualifiedName(Rss20Constants.UrlTag, string.Empty);
        private const string Rfc822OutputLocalDateTimeFormat = "ddd, dd MMM yyyy HH:mm:ss zzz";
        private const string Rfc822OutputUtcDateTimeFormat = "ddd, dd MMM yyyy HH:mm:ss Z";
 
        private readonly Atom10FeedFormatter _atomSerializer;
        private readonly int _maxExtensionSize;
 
        public Rss20FeedFormatter() : this(typeof(SyndicationFeed))
        {
        }
 
        public Rss20FeedFormatter(Type feedTypeToCreate) : base()
        {
            if (feedTypeToCreate is null)
            {
                throw new ArgumentNullException(nameof(feedTypeToCreate));
            }
 
            if (!typeof(SyndicationFeed).IsAssignableFrom(feedTypeToCreate))
            {
                throw new ArgumentException(SR.Format(SR.InvalidObjectTypePassed, nameof(feedTypeToCreate), nameof(SyndicationFeed)), nameof(feedTypeToCreate));
            }
 
            SerializeExtensionsAsAtom = true;
            _maxExtensionSize = int.MaxValue;
            _atomSerializer = new Atom10FeedFormatter(feedTypeToCreate);
            FeedType = feedTypeToCreate;
        }
 
        public Rss20FeedFormatter(SyndicationFeed feedToWrite) : this(feedToWrite, true)
        {
        }
 
        public Rss20FeedFormatter(SyndicationFeed feedToWrite, bool serializeExtensionsAsAtom) : base(feedToWrite)
        {
            // No need to check that the parameter passed is valid - it is checked by the c'tor of the base class
            SerializeExtensionsAsAtom = serializeExtensionsAsAtom;
            _maxExtensionSize = int.MaxValue;
            _atomSerializer = new Atom10FeedFormatter(Feed);
            FeedType = feedToWrite.GetType();
        }
 
        internal override TryParseDateTimeCallback GetDefaultDateTimeParser() => DateTimeHelper.DefaultRss20DateTimeParser;
 
        public bool PreserveAttributeExtensions { get; set; } = true;
 
        public bool PreserveElementExtensions { get; set; } = true;
 
        public bool SerializeExtensionsAsAtom { get; set; }
 
        public override string Version => SyndicationVersions.Rss20;
 
        protected Type FeedType { get; }
 
        public override bool CanRead(XmlReader reader)
        {
            if (reader is null)
            {
                throw new ArgumentNullException(nameof(reader));
            }
 
            return reader.IsStartElement(Rss20Constants.RssTag, Rss20Constants.Rss20Namespace);
        }
 
        XmlSchema IXmlSerializable.GetSchema() => null;
 
        void IXmlSerializable.ReadXml(XmlReader reader)
        {
            if (reader is null)
            {
                throw new ArgumentNullException(nameof(reader));
            }
 
            ReadFeed(reader);
        }
 
        void IXmlSerializable.WriteXml(XmlWriter writer)
        {
            if (writer is null)
            {
                throw new ArgumentNullException(nameof(writer));
            }
 
            WriteFeed(writer);
        }
 
        public override void ReadFrom(XmlReader reader)
        {
            if (!CanRead(reader))
            {
                throw new XmlException(SR.Format(SR.UnknownFeedXml, reader.LocalName, reader.NamespaceURI));
            }
 
            ReadFeed(reader);
        }
 
        public override void WriteTo(XmlWriter writer)
        {
            if (writer is null)
            {
                throw new ArgumentNullException(nameof(writer));
            }
 
            writer.WriteStartElement(Rss20Constants.RssTag, Rss20Constants.Rss20Namespace);
            WriteFeed(writer);
            writer.WriteEndElement();
        }
 
        protected internal override void SetFeed(SyndicationFeed feed)
        {
            base.SetFeed(feed);
            _atomSerializer.SetFeed(Feed);
        }
 
        internal void ReadItemFrom(XmlReader reader, SyndicationItem result)
        {
            ReadItemFrom(reader, result, null);
        }
 
        internal void WriteItemContents(XmlWriter writer, SyndicationItem item)
        {
            WriteItemContents(writer, item, null);
        }
 
        protected override SyndicationFeed CreateFeedInstance() => CreateFeedInstance(FeedType);
 
        protected virtual SyndicationItem ReadItem(XmlReader reader, SyndicationFeed feed)
        {
            if (reader is null)
            {
                throw new ArgumentNullException(nameof(reader));
            }
            if (feed is null)
            {
                throw new ArgumentNullException(nameof(feed));
            }
 
            SyndicationItem item = CreateItem(feed);
            ReadItemFrom(reader, item, feed.BaseUri);
            return item;
        }
 
        protected virtual IEnumerable<SyndicationItem> ReadItems(XmlReader reader, SyndicationFeed feed, out bool areAllItemsRead)
        {
            if (reader is null)
            {
                throw new ArgumentNullException(nameof(reader));
            }
            if (feed is null)
            {
                throw new ArgumentNullException(nameof(feed));
            }
 
            NullNotAllowedCollection<SyndicationItem> items = new NullNotAllowedCollection<SyndicationItem>();
            while (reader.IsStartElement(Rss20Constants.ItemTag, Rss20Constants.Rss20Namespace))
            {
                items.Add(ReadItem(reader, feed));
            }
            areAllItemsRead = true;
            return items;
        }
 
        protected virtual void WriteItem(XmlWriter writer, SyndicationItem item, Uri feedBaseUri)
        {
            writer.WriteStartElement(Rss20Constants.ItemTag, Rss20Constants.Rss20Namespace);
            WriteItemContents(writer, item, feedBaseUri);
            writer.WriteEndElement();
        }
 
        protected virtual void WriteItems(XmlWriter writer, IEnumerable<SyndicationItem> items, Uri feedBaseUri)
        {
            if (items == null)
            {
                return;
            }
 
            foreach (SyndicationItem item in items)
            {
                WriteItem(writer, item, feedBaseUri);
            }
        }
 
        private static string AsString(DateTimeOffset dateTime)
        {
#if NET8_0_OR_GREATER
            if (dateTime.TotalOffsetMinutes == 0)
#else
            if (dateTime.Offset == TimeSpan.Zero)
#endif // NET8_0_OR_GREATER
            {
                return dateTime.ToUniversalTime().ToString(Rfc822OutputUtcDateTimeFormat, CultureInfo.InvariantCulture);
            }
            else
            {
                StringBuilder sb = new StringBuilder(dateTime.ToString(Rfc822OutputLocalDateTimeFormat, CultureInfo.InvariantCulture));
                // the zzz in Rfc822OutputLocalDateTimeFormat makes the timezone e.g. "-08:00" but we require e.g. "-0800" without the ':'
                sb.Remove(sb.Length - 3, 1);
                return sb.ToString();
            }
        }
 
        internal static SyndicationLink ReadAlternateLink(XmlReader reader, Uri baseUri, TryParseUriCallback uriParser, bool preserveAttributeExtensions)
        {
            var link = new SyndicationLink
            {
                BaseUri = baseUri,
                RelationshipType = Atom10Constants.AlternateTag
            };
            if (reader.HasAttributes)
            {
                while (reader.MoveToNextAttribute())
                {
                    if (reader.LocalName == "base" && reader.NamespaceURI == Atom10FeedFormatter.XmlNs)
                    {
                        link.BaseUri = FeedUtils.CombineXmlBase(link.BaseUri, reader.Value);
                    }
                    else if (!FeedUtils.IsXmlns(reader.LocalName, reader.NamespaceURI))
                    {
                        if (preserveAttributeExtensions)
                        {
                            link.AttributeExtensions.Add(new XmlQualifiedName(reader.LocalName, reader.NamespaceURI), reader.Value);
                        }
                    }
                }
            }
 
            string uriString = reader.ReadElementString();
            Uri uri = UriFromString(uriParser, uriString, UriKind.RelativeOrAbsolute, Rss20Constants.LinkTag, Rss20Constants.Rss20Namespace, reader);
            link.Uri = uri;
            return link;
        }
 
        private SyndicationCategory ReadCategory(XmlReader reader, SyndicationFeed feed)
        {
            SyndicationCategory result = CreateCategory(feed);
            ReadCategory(reader, result);
            return result;
        }
 
        private SyndicationCategory ReadCategory(XmlReader reader, SyndicationItem item)
        {
            SyndicationCategory result = CreateCategory(item);
            ReadCategory(reader, result);
            return result;
        }
 
        private void ReadCategory(XmlReader reader, SyndicationCategory category)
        {
            bool isEmpty = reader.IsEmptyElement;
            if (reader.HasAttributes)
            {
                while (reader.MoveToNextAttribute())
                {
                    string ns = reader.NamespaceURI;
                    string name = reader.LocalName;
                    if (FeedUtils.IsXmlns(name, ns))
                    {
                        continue;
                    }
                    string val = reader.Value;
                    if (name == Rss20Constants.DomainTag && ns == Rss20Constants.Rss20Namespace)
                    {
                        category.Scheme = val;
                    }
                    else if (!TryParseAttribute(name, ns, val, category, Version))
                    {
                        if (PreserveAttributeExtensions)
                        {
                            category.AttributeExtensions.Add(new XmlQualifiedName(name, ns), val);
                        }
                    }
                }
            }
            reader.ReadStartElement(Rss20Constants.CategoryTag, Rss20Constants.Rss20Namespace);
            if (!isEmpty)
            {
                category.Name = reader.ReadString();
                reader.ReadEndElement();
            }
        }
 
        private void ReadFeed(XmlReader reader)
        {
            SetFeed(CreateFeedInstance());
            ReadXml(reader, Feed);
        }
 
        private void ReadItemFrom(XmlReader reader, SyndicationItem result, Uri feedBaseUri)
        {
            try
            {
                result.BaseUri = feedBaseUri;
                reader.MoveToContent();
                bool isEmpty = reader.IsEmptyElement;
                if (reader.HasAttributes)
                {
                    while (reader.MoveToNextAttribute())
                    {
                        string ns = reader.NamespaceURI;
                        string name = reader.LocalName;
                        if (name == "base" && ns == Atom10FeedFormatter.XmlNs)
                        {
                            result.BaseUri = FeedUtils.CombineXmlBase(result.BaseUri, reader.Value);
                            continue;
                        }
                        if (FeedUtils.IsXmlns(name, ns) || FeedUtils.IsXmlSchemaType(name, ns))
                        {
                            continue;
                        }
                        string val = reader.Value;
                        if (!TryParseAttribute(name, ns, val, result, Version))
                        {
                            if (PreserveAttributeExtensions)
                            {
                                result.AttributeExtensions.Add(new XmlQualifiedName(name, ns), val);
                            }
                        }
                    }
                }
                reader.ReadStartElement();
                if (!isEmpty)
                {
                    string fallbackAlternateLink = null;
                    string fallbackAlternateLinkLocalName = null;
                    string fallbackAlternateLinkNamespace = null;
                    XmlDictionaryWriter extWriter = null;
                    bool readAlternateLink = false;
                    try
                    {
                        XmlBuffer buffer = null;
                        while (reader.IsStartElement())
                        {
                            if (reader.IsStartElement(Rss20Constants.TitleTag, Rss20Constants.Rss20Namespace))
                            {
                                result.Title = new TextSyndicationContent(reader.ReadElementString());
                            }
                            else if (reader.IsStartElement(Rss20Constants.LinkTag, Rss20Constants.Rss20Namespace))
                            {
                                result.Links.Add(ReadAlternateLink(reader, result.BaseUri, UriParser, PreserveAttributeExtensions));
                                readAlternateLink = true;
                            }
                            else if (reader.IsStartElement(Rss20Constants.DescriptionTag, Rss20Constants.Rss20Namespace))
                            {
                                result.Summary = new TextSyndicationContent(reader.ReadElementString());
                            }
                            else if (reader.IsStartElement(Rss20Constants.AuthorTag, Rss20Constants.Rss20Namespace))
                            {
                                result.Authors.Add(ReadPerson(reader, result));
                            }
                            else if (reader.IsStartElement(Rss20Constants.CategoryTag, Rss20Constants.Rss20Namespace))
                            {
                                result.Categories.Add(ReadCategory(reader, result));
                            }
                            else if (reader.IsStartElement(Rss20Constants.EnclosureTag, Rss20Constants.Rss20Namespace))
                            {
                                result.Links.Add(ReadMediaEnclosure(reader, result.BaseUri));
                            }
                            else if (reader.IsStartElement(Rss20Constants.GuidTag, Rss20Constants.Rss20Namespace))
                            {
                                bool isPermalink = true;
                                string permalinkString = reader.GetAttribute(Rss20Constants.IsPermaLinkTag, Rss20Constants.Rss20Namespace);
                                if (permalinkString != null && permalinkString.Equals("FALSE", StringComparison.InvariantCultureIgnoreCase))
                                {
                                    isPermalink = false;
                                }
 
                                result.Id = reader.ReadElementString();
                                if (isPermalink)
                                {
                                    fallbackAlternateLink = result.Id;
                                    fallbackAlternateLinkLocalName = Rss20Constants.GuidTag;
                                    fallbackAlternateLinkNamespace = Rss20Constants.Rss20Namespace;
                                }
                            }
                            else if (reader.IsStartElement(Rss20Constants.PubDateTag, Rss20Constants.Rss20Namespace))
                            {
                                bool canReadContent = !reader.IsEmptyElement;
                                reader.ReadStartElement();
                                if (canReadContent)
                                {
                                    string str = reader.ReadString();
                                    if (!string.IsNullOrEmpty(str))
                                    {
                                        try
                                        {
                                            result.PublishDate = DateFromString(str, reader);
                                        }
                                        catch (XmlException e)
                                        {
                                            result.PublishDateException = e;
                                        }
                                    }
                                    reader.ReadEndElement();
                                }
                            }
                            else if (reader.IsStartElement(Rss20Constants.SourceTag, Rss20Constants.Rss20Namespace))
                            {
                                SyndicationFeed feed = new SyndicationFeed();
                                if (reader.HasAttributes)
                                {
                                    while (reader.MoveToNextAttribute())
                                    {
                                        string ns = reader.NamespaceURI;
                                        string name = reader.LocalName;
                                        if (FeedUtils.IsXmlns(name, ns))
                                        {
                                            continue;
                                        }
                                        string val = reader.Value;
                                        if (name == Rss20Constants.UrlTag && ns == Rss20Constants.Rss20Namespace)
                                        {
                                            feed.Links.Add(SyndicationLink.CreateSelfLink(UriFromString(val, UriKind.RelativeOrAbsolute, Rss20Constants.UrlTag, Rss20Constants.Rss20Namespace, reader)));
                                        }
                                        else if (!FeedUtils.IsXmlns(name, ns))
                                        {
                                            if (PreserveAttributeExtensions)
                                            {
                                                feed.AttributeExtensions.Add(new XmlQualifiedName(name, ns), val);
                                            }
                                        }
                                    }
                                }
 
                                string feedTitle = reader.ReadElementString();
                                feed.Title = new TextSyndicationContent(feedTitle);
                                result.SourceFeed = feed;
                            }
                            else
                            {
                                bool parsedExtension = SerializeExtensionsAsAtom && _atomSerializer.TryParseItemElementFrom(reader, result);
                                if (!parsedExtension)
                                {
                                    parsedExtension = TryParseElement(reader, result, Version);
                                }
                                if (!parsedExtension)
                                {
                                    if (PreserveElementExtensions)
                                    {
                                        CreateBufferIfRequiredAndWriteNode(ref buffer, ref extWriter, reader, _maxExtensionSize);
                                    }
                                    else
                                    {
                                        reader.Skip();
                                    }
                                }
                            }
                        }
                        LoadElementExtensions(buffer, extWriter, result);
                    }
                    finally
                    {
                        extWriter?.Dispose();
                    }
 
                    reader.ReadEndElement(); // item
                    if (!readAlternateLink && fallbackAlternateLink != null)
                    {
                        result.Links.Add(SyndicationLink.CreateAlternateLink(UriFromString(fallbackAlternateLink, UriKind.RelativeOrAbsolute, fallbackAlternateLinkLocalName, fallbackAlternateLinkNamespace, reader)));
                        readAlternateLink = true;
                    }
 
                    // if there's no content and no alternate link set the summary as the item content
                    if (result.Content == null && !readAlternateLink)
                    {
                        result.Content = result.Summary;
                        result.Summary = null;
                    }
                }
            }
            catch (FormatException e)
            {
                throw new XmlException(FeedUtils.AddLineInfo(reader, SR.ErrorParsingItem), e);
            }
            catch (ArgumentException e)
            {
                throw new XmlException(FeedUtils.AddLineInfo(reader, SR.ErrorParsingItem), e);
            }
        }
 
        private SyndicationLink ReadMediaEnclosure(XmlReader reader, Uri baseUri)
        {
            var link = new SyndicationLink
            {
                BaseUri = baseUri,
                RelationshipType = Rss20Constants.EnclosureTag
            };
            bool isEmptyElement = reader.IsEmptyElement;
            if (reader.HasAttributes)
            {
                while (reader.MoveToNextAttribute())
                {
                    string ns = reader.NamespaceURI;
                    string name = reader.LocalName;
                    if (name == "base" && ns == Atom10FeedFormatter.XmlNs)
                    {
                        link.BaseUri = FeedUtils.CombineXmlBase(link.BaseUri, reader.Value);
                        continue;
                    }
                    if (FeedUtils.IsXmlns(name, ns))
                    {
                        continue;
                    }
                    string val = reader.Value;
                    if (name == Rss20Constants.UrlTag && ns == Rss20Constants.Rss20Namespace)
                    {
                        link.Uri = UriFromString(val, UriKind.RelativeOrAbsolute, Rss20Constants.EnclosureTag, Rss20Constants.Rss20Namespace, reader);
                    }
                    else if (name == Rss20Constants.TypeTag && ns == Rss20Constants.Rss20Namespace)
                    {
                        link.MediaType = val;
                    }
                    else if (name == Rss20Constants.LengthTag && ns == Rss20Constants.Rss20Namespace)
                    {
                        link.Length = !string.IsNullOrEmpty(val) ? Convert.ToInt64(val, CultureInfo.InvariantCulture.NumberFormat) : 0;
                    }
                    else if (!FeedUtils.IsXmlns(name, ns))
                    {
                        if (PreserveAttributeExtensions)
                        {
                            link.AttributeExtensions.Add(new XmlQualifiedName(name, ns), val);
                        }
                    }
                }
            }
            reader.ReadStartElement(Rss20Constants.EnclosureTag, Rss20Constants.Rss20Namespace);
            if (!isEmptyElement)
            {
                reader.ReadEndElement();
            }
            return link;
        }
 
        private SyndicationPerson ReadPerson(XmlReader reader, SyndicationFeed feed)
        {
            SyndicationPerson result = CreatePerson(feed);
            ReadPerson(reader, result);
            return result;
        }
 
        private SyndicationPerson ReadPerson(XmlReader reader, SyndicationItem item)
        {
            SyndicationPerson result = CreatePerson(item);
            ReadPerson(reader, result);
            return result;
        }
 
        private void ReadPerson(XmlReader reader, SyndicationPerson person)
        {
            bool isEmpty = reader.IsEmptyElement;
            if (reader.HasAttributes)
            {
                while (reader.MoveToNextAttribute())
                {
                    string ns = reader.NamespaceURI;
                    string name = reader.LocalName;
                    if (FeedUtils.IsXmlns(name, ns))
                    {
                        continue;
                    }
                    string val = reader.Value;
                    if (!TryParseAttribute(name, ns, val, person, Version))
                    {
                        if (PreserveAttributeExtensions)
                        {
                            person.AttributeExtensions.Add(new XmlQualifiedName(name, ns), val);
                        }
                    }
                }
            }
            reader.ReadStartElement();
            if (!isEmpty)
            {
                string email = reader.ReadString();
                reader.ReadEndElement();
                person.Email = email;
            }
        }
 
        private void ReadXml(XmlReader reader, SyndicationFeed result)
        {
            try
            {
                string baseUri = null;
                string baseUriLocalName = null;
                string baseUriNamespace = null;
                reader.MoveToContent();
                string elementLocalName = reader.LocalName;
                string elementNamespace = reader.NamespaceURI;
                string version = reader.GetAttribute(Rss20Constants.VersionTag, Rss20Constants.Rss20Namespace);
                if (version != Rss20Constants.Version)
                {
                    throw new NotSupportedException(FeedUtils.AddLineInfo(reader, (SR.Format(SR.UnsupportedRssVersion, version))));
                }
                if (reader.AttributeCount > 1)
                {
                    string tmp = reader.GetAttribute("base", Atom10FeedFormatter.XmlNs);
                    if (!string.IsNullOrEmpty(tmp))
                    {
                        baseUri = tmp;
                        baseUriLocalName = elementLocalName;
                        baseUriNamespace = elementNamespace;
                    }
                }
                reader.ReadStartElement();
                reader.MoveToContent();
                elementLocalName = reader.LocalName;
                elementNamespace = reader.NamespaceURI;
                if (reader.HasAttributes)
                {
                    while (reader.MoveToNextAttribute())
                    {
                        string ns = reader.NamespaceURI;
                        string name = reader.LocalName;
                        if (name == "base" && ns == Atom10FeedFormatter.XmlNs)
                        {
                            baseUri = reader.Value;
                            baseUriLocalName = elementLocalName;
                            baseUriNamespace = elementNamespace;
                            continue;
                        }
                        if (FeedUtils.IsXmlns(name, ns) || FeedUtils.IsXmlSchemaType(name, ns))
                        {
                            continue;
                        }
                        string val = reader.Value;
                        if (!TryParseAttribute(name, ns, val, result, Version))
                        {
                            if (PreserveAttributeExtensions)
                            {
                                result.AttributeExtensions.Add(new XmlQualifiedName(name, ns), val);
                            }
                        }
                    }
                }
 
                if (!string.IsNullOrEmpty(baseUri))
                {
                    result.BaseUri = UriFromString(baseUri, UriKind.RelativeOrAbsolute, baseUriLocalName, baseUriNamespace, reader);
                }
 
                bool areAllItemsRead = true;
                reader.ReadStartElement(Rss20Constants.ChannelTag, Rss20Constants.Rss20Namespace);
 
                XmlBuffer buffer = null;
                XmlDictionaryWriter extWriter = null;
                NullNotAllowedCollection<SyndicationItem> feedItems = null;
 
                try
                {
                    while (reader.IsStartElement())
                    {
                        if (reader.IsStartElement(Rss20Constants.TitleTag, Rss20Constants.Rss20Namespace))
                        {
                            result.Title = new TextSyndicationContent(reader.ReadElementString());
                        }
                        else if (reader.IsStartElement(Rss20Constants.LinkTag, Rss20Constants.Rss20Namespace))
                        {
                            result.Links.Add(ReadAlternateLink(reader, result.BaseUri, UriParser, PreserveAttributeExtensions));
                        }
                        else if (reader.IsStartElement(Rss20Constants.DescriptionTag, Rss20Constants.Rss20Namespace))
                        {
                            result.Description = new TextSyndicationContent(reader.ReadElementString());
                        }
                        else if (reader.IsStartElement(Rss20Constants.LanguageTag, Rss20Constants.Rss20Namespace))
                        {
                            result.Language = reader.ReadElementString();
                        }
                        else if (reader.IsStartElement(Rss20Constants.CopyrightTag, Rss20Constants.Rss20Namespace))
                        {
                            result.Copyright = new TextSyndicationContent(reader.ReadElementString());
                        }
                        else if (reader.IsStartElement(Rss20Constants.ManagingEditorTag, Rss20Constants.Rss20Namespace))
                        {
                            result.Authors.Add(ReadPerson(reader, result));
                        }
                        else if (reader.IsStartElement(Rss20Constants.LastBuildDateTag, Rss20Constants.Rss20Namespace))
                        {
                            bool canReadContent = !reader.IsEmptyElement;
                            reader.ReadStartElement();
                            if (canReadContent)
                            {
                                string str = reader.ReadString();
                                if (!string.IsNullOrEmpty(str))
                                {
                                    try
                                    {
                                        result.LastUpdatedTime = DateFromString(str, reader);
                                    }
                                    catch (XmlException e)
                                    {
                                        result.LastUpdatedTimeException = e;
                                    }
                                }
                                reader.ReadEndElement();
                            }
                        }
                        else if (reader.IsStartElement(Rss20Constants.CategoryTag, Rss20Constants.Rss20Namespace))
                        {
                            result.Categories.Add(ReadCategory(reader, result));
                        }
                        else if (reader.IsStartElement(Rss20Constants.GeneratorTag, Rss20Constants.Rss20Namespace))
                        {
                            result.Generator = reader.ReadElementString();
                        }
                        else if (reader.IsStartElement(Rss20Constants.ImageTag, Rss20Constants.Rss20Namespace))
                        {
                            reader.ReadStartElement();
                            while (reader.IsStartElement())
                            {
                                if (reader.IsStartElement(Rss20Constants.UrlTag, Rss20Constants.Rss20Namespace))
                                {
                                    result.ImageUrl = UriFromString(reader.ReadElementString(), UriKind.RelativeOrAbsolute, Rss20Constants.UrlTag, Rss20Constants.Rss20Namespace, reader);
                                }
                                else
                                {
                                    // ignore other content
                                    reader.Skip();
                                }
                            }
                            reader.ReadEndElement(); // image
                        }
                        else if (reader.IsStartElement(Rss20Constants.ItemTag, Rss20Constants.Rss20Namespace))
                        {
                            feedItems ??= new NullNotAllowedCollection<SyndicationItem>();
                            IEnumerable<SyndicationItem> items = ReadItems(reader, result, out areAllItemsRead);
                            foreach (SyndicationItem item in items)
                            {
                                feedItems.Add(item);
                            }
 
                            // if the derived class is reading the items lazily, then stop reading from the stream
                            if (!areAllItemsRead)
                            {
                                break;
                            }
                        }
                        else
                        {
                            bool parsedExtension = SerializeExtensionsAsAtom && _atomSerializer.TryParseFeedElementFrom(reader, result);
                            if (!parsedExtension)
                            {
                                parsedExtension = TryParseElement(reader, result, Version);
                            }
                            if (!parsedExtension)
                            {
                                if (PreserveElementExtensions)
                                {
                                    CreateBufferIfRequiredAndWriteNode(ref buffer, ref extWriter, reader, _maxExtensionSize);
                                }
                                else
                                {
                                    reader.Skip();
                                }
                            }
                        }
                    }
 
                    if (feedItems != null)
                    {
                        result.Items = feedItems;
                    }
 
                    LoadElementExtensions(buffer, extWriter, result);
                }
                finally
                {
                    extWriter?.Dispose();
                }
                if (areAllItemsRead)
                {
                    reader.ReadEndElement(); // channel
                    reader.ReadEndElement(); // rss
                }
            }
            catch (FormatException e)
            {
                throw new XmlException(FeedUtils.AddLineInfo(reader, SR.ErrorParsingFeed), e);
            }
            catch (ArgumentException e)
            {
                throw new XmlException(FeedUtils.AddLineInfo(reader, SR.ErrorParsingFeed), e);
            }
        }
 
        private static void WriteAlternateLink(XmlWriter writer, SyndicationLink link, Uri baseUri)
        {
            writer.WriteStartElement(Rss20Constants.LinkTag, Rss20Constants.Rss20Namespace);
            Uri baseUriToWrite = FeedUtils.GetBaseUriToWrite(baseUri, link.BaseUri);
            if (baseUriToWrite != null)
            {
                writer.WriteAttributeString("xml", "base", Atom10FeedFormatter.XmlNs, FeedUtils.GetUriString(baseUriToWrite));
            }
            link.WriteAttributeExtensions(writer, SyndicationVersions.Rss20);
            writer.WriteString(FeedUtils.GetUriString(link.Uri));
            writer.WriteEndElement();
        }
 
        private void WriteCategory(XmlWriter writer, SyndicationCategory category)
        {
            if (category == null)
            {
                return;
            }
            writer.WriteStartElement(Rss20Constants.CategoryTag, Rss20Constants.Rss20Namespace);
            WriteAttributeExtensions(writer, category, Version);
            if (!string.IsNullOrEmpty(category.Scheme) && !category.AttributeExtensions.ContainsKey(s_rss20Domain))
            {
                writer.WriteAttributeString(Rss20Constants.DomainTag, Rss20Constants.Rss20Namespace, category.Scheme);
            }
            writer.WriteString(category.Name);
            writer.WriteEndElement();
        }
 
        private void WriteFeed(XmlWriter writer)
        {
            if (Feed == null)
            {
                throw new InvalidOperationException(SR.FeedFormatterDoesNotHaveFeed);
            }
 
            if (SerializeExtensionsAsAtom)
            {
                writer.WriteAttributeString("xmlns", Atom10Constants.Atom10Prefix, null, Atom10Constants.Atom10Namespace);
            }
            writer.WriteAttributeString(Rss20Constants.VersionTag, Rss20Constants.Version);
            writer.WriteStartElement(Rss20Constants.ChannelTag, Rss20Constants.Rss20Namespace);
            if (Feed.BaseUri != null)
            {
                writer.WriteAttributeString("xml", "base", Atom10FeedFormatter.XmlNs, FeedUtils.GetUriString(Feed.BaseUri));
            }
            WriteAttributeExtensions(writer, Feed, Version);
            string title = Feed.Title != null ? Feed.Title.Text : string.Empty;
            writer.WriteElementString(Rss20Constants.TitleTag, Rss20Constants.Rss20Namespace, title);
 
            SyndicationLink alternateLink = null;
            for (int i = 0; i < Feed.Links.Count; ++i)
            {
                if (Feed.Links[i].RelationshipType == Atom10Constants.AlternateTag)
                {
                    alternateLink = Feed.Links[i];
                    WriteAlternateLink(writer, alternateLink, Feed.BaseUri);
                    break;
                }
            }
 
            string description = Feed.Description != null ? Feed.Description.Text : string.Empty;
            writer.WriteElementString(Rss20Constants.DescriptionTag, Rss20Constants.Rss20Namespace, description);
 
            if (Feed.Language != null)
            {
                writer.WriteElementString(Rss20Constants.LanguageTag, Feed.Language);
            }
 
            if (Feed.Copyright != null)
            {
                writer.WriteElementString(Rss20Constants.CopyrightTag, Rss20Constants.Rss20Namespace, Feed.Copyright.Text);
            }
 
            // if there's a single author with an email address, then serialize as the managingEditor
            // else serialize the authors as Atom extensions
            if ((Feed.Authors.Count == 1) && (Feed.Authors[0].Email != null))
            {
                WritePerson(writer, Rss20Constants.ManagingEditorTag, Feed.Authors[0]);
            }
            else
            {
                if (SerializeExtensionsAsAtom)
                {
                    _atomSerializer.WriteFeedAuthorsTo(writer, Feed.Authors);
                }
            }
 
            if (Feed.LastUpdatedTime > DateTimeOffset.MinValue)
            {
                writer.WriteStartElement(Rss20Constants.LastBuildDateTag);
                writer.WriteString(AsString(Feed.LastUpdatedTime));
                writer.WriteEndElement();
            }
 
            for (int i = 0; i < Feed.Categories.Count; ++i)
            {
                WriteCategory(writer, Feed.Categories[i]);
            }
 
            if (!string.IsNullOrEmpty(Feed.Generator))
            {
                writer.WriteElementString(Rss20Constants.GeneratorTag, Feed.Generator);
            }
 
            if (Feed.Contributors.Count > 0)
            {
                if (SerializeExtensionsAsAtom)
                {
                    _atomSerializer.WriteFeedContributorsTo(writer, Feed.Contributors);
                }
            }
 
            if (Feed.ImageUrl != null)
            {
                writer.WriteStartElement(Rss20Constants.ImageTag);
                writer.WriteElementString(Rss20Constants.UrlTag, FeedUtils.GetUriString(Feed.ImageUrl));
                writer.WriteElementString(Rss20Constants.TitleTag, Rss20Constants.Rss20Namespace, title);
                string imgAlternateLink = (alternateLink != null) ? FeedUtils.GetUriString(alternateLink.Uri) : string.Empty;
                writer.WriteElementString(Rss20Constants.LinkTag, Rss20Constants.Rss20Namespace, imgAlternateLink);
                writer.WriteEndElement(); // image
            }
 
            // Optional spec items
            if (Feed.InternalDocumentation?.Uri != null)
            {
                writer.WriteElementString(Rss20Constants.DocumentationTag, Feed.InternalDocumentation.Uri.ToString());
            }
 
            if (Feed.InternalTimeToLive != null)
            {
                writer.WriteElementString(Rss20Constants.TimeToLiveTag, ((int)Feed.InternalTimeToLive.Value.TotalMinutes).ToString());
            }
 
            if (Feed.InternalSkipHours?.Count > 0)
            {
                writer.WriteStartElement(Rss20Constants.SkipHoursTag);
 
                foreach (int hour in Feed.InternalSkipHours)
                {
                    writer.WriteElementString(Rss20Constants.HourTag, hour.ToString());
                }
 
                writer.WriteEndElement();
            }
 
            if (Feed.InternalSkipDays?.Count > 0)
            {
                writer.WriteStartElement(Rss20Constants.SkipDaysTag);
 
                foreach (string day in Feed.InternalSkipDays)
                {
                    writer.WriteElementString(Rss20Constants.DayTag, day);
                }
 
                writer.WriteEndElement();
            }
 
            if (Feed.InternalTextInput != null)
            {
                writer.WriteStartElement(Rss20Constants.TextInputTag);
 
                writer.WriteElementString(Rss20Constants.DescriptionTag, Feed.InternalTextInput.Description);
                writer.WriteElementString(Rss20Constants.TitleTag, Feed.InternalTextInput.Title);
                writer.WriteElementString(Rss20Constants.LinkTag, Feed.InternalTextInput.Link.GetAbsoluteUri().ToString());
                writer.WriteElementString(Rss20Constants.NameTag, Feed.InternalTextInput.Name);
 
                writer.WriteEndElement();
            }
 
            if (SerializeExtensionsAsAtom)
            {
                Atom10FeedFormatter.WriteElement(writer, Atom10Constants.IdTag, Feed.Id);
 
                // dont write out the 1st alternate link since that would have been written out anyway
                bool isFirstAlternateLink = true;
                for (int i = 0; i < Feed.Links.Count; ++i)
                {
                    if (Feed.Links[i].RelationshipType == Atom10Constants.AlternateTag && isFirstAlternateLink)
                    {
                        isFirstAlternateLink = false;
                        continue;
                    }
                    Atom10FeedFormatter.WriteLink(writer, Feed.Links[i], Feed.BaseUri);
                }
            }
 
            WriteElementExtensions(writer, Feed, Version);
            WriteItems(writer, Feed.Items, Feed.BaseUri);
            writer.WriteEndElement(); // channel
        }
 
        private void WriteItemContents(XmlWriter writer, SyndicationItem item, Uri feedBaseUri)
        {
            Uri baseUriToWrite = FeedUtils.GetBaseUriToWrite(feedBaseUri, item.BaseUri);
            if (baseUriToWrite != null)
            {
                writer.WriteAttributeString("xml", "base", Atom10FeedFormatter.XmlNs, FeedUtils.GetUriString(baseUriToWrite));
            }
            WriteAttributeExtensions(writer, item, Version);
            string guid = item.Id ?? string.Empty;
            bool isPermalink = false;
            SyndicationLink firstAlternateLink = null;
            for (int i = 0; i < item.Links.Count; ++i)
            {
                if (item.Links[i].RelationshipType == Atom10Constants.AlternateTag)
                {
                    firstAlternateLink ??= item.Links[i];
                    if (guid == FeedUtils.GetUriString(item.Links[i].Uri))
                    {
                        isPermalink = true;
                        break;
                    }
                }
            }
            if (!string.IsNullOrEmpty(guid))
            {
                writer.WriteStartElement(Rss20Constants.GuidTag);
                if (isPermalink)
                {
                    writer.WriteAttributeString(Rss20Constants.IsPermaLinkTag, "true");
                }
                else
                {
                    writer.WriteAttributeString(Rss20Constants.IsPermaLinkTag, "false");
                }
                writer.WriteString(guid);
                writer.WriteEndElement();
            }
            if (firstAlternateLink != null)
            {
                WriteAlternateLink(writer, firstAlternateLink, (item.BaseUri ?? feedBaseUri));
            }
 
            if (item.Authors.Count == 1 && !string.IsNullOrEmpty(item.Authors[0].Email))
            {
                WritePerson(writer, Rss20Constants.AuthorTag, item.Authors[0]);
            }
            else
            {
                if (SerializeExtensionsAsAtom)
                {
                    _atomSerializer.WriteItemAuthorsTo(writer, item.Authors);
                }
            }
 
            for (int i = 0; i < item.Categories.Count; ++i)
            {
                WriteCategory(writer, item.Categories[i]);
            }
 
            bool serializedTitle = false;
            if (item.Title != null)
            {
                writer.WriteElementString(Rss20Constants.TitleTag, item.Title.Text);
                serializedTitle = true;
            }
 
            bool serializedContentAsDescription = false;
            TextSyndicationContent summary = item.Summary;
            if (summary == null)
            {
                summary = (item.Content as TextSyndicationContent);
                serializedContentAsDescription = (summary != null);
            }
            // the spec requires the wire to have a title or a description
            if (!serializedTitle && summary == null)
            {
                summary = new TextSyndicationContent(string.Empty);
            }
            if (summary != null)
            {
                writer.WriteElementString(Rss20Constants.DescriptionTag, Rss20Constants.Rss20Namespace, summary.Text);
            }
 
            if (item.SourceFeed != null)
            {
                writer.WriteStartElement(Rss20Constants.SourceTag, Rss20Constants.Rss20Namespace);
                WriteAttributeExtensions(writer, item.SourceFeed, Version);
                SyndicationLink selfLink = null;
                for (int i = 0; i < item.SourceFeed.Links.Count; ++i)
                {
                    if (item.SourceFeed.Links[i].RelationshipType == Atom10Constants.SelfTag)
                    {
                        selfLink = item.SourceFeed.Links[i];
                        break;
                    }
                }
                if (selfLink != null && !item.SourceFeed.AttributeExtensions.ContainsKey(s_rss20Url))
                {
                    writer.WriteAttributeString(Rss20Constants.UrlTag, Rss20Constants.Rss20Namespace, FeedUtils.GetUriString(selfLink.Uri));
                }
                string title = (item.SourceFeed.Title != null) ? item.SourceFeed.Title.Text : string.Empty;
                writer.WriteString(title);
                writer.WriteEndElement();
            }
 
            if (item.PublishDate > DateTimeOffset.MinValue)
            {
                writer.WriteElementString(Rss20Constants.PubDateTag, Rss20Constants.Rss20Namespace, AsString(item.PublishDate));
            }
 
            // serialize the enclosures
            SyndicationLink firstEnclosureLink = null;
            bool passedFirstAlternateLink = false;
            for (int i = 0; i < item.Links.Count; ++i)
            {
                if (item.Links[i].RelationshipType == Rss20Constants.EnclosureTag)
                {
                    if (firstEnclosureLink == null)
                    {
                        firstEnclosureLink = item.Links[i];
                        WriteMediaEnclosure(writer, item.Links[i], item.BaseUri);
                        continue;
                    }
                }
                else if (item.Links[i].RelationshipType == Atom10Constants.AlternateTag)
                {
                    if (!passedFirstAlternateLink)
                    {
                        passedFirstAlternateLink = true;
                        continue;
                    }
                }
                if (SerializeExtensionsAsAtom)
                {
                    Atom10FeedFormatter.WriteLink(writer, item.Links[i], item.BaseUri);
                }
            }
 
            if (item.LastUpdatedTime > DateTimeOffset.MinValue)
            {
                if (SerializeExtensionsAsAtom)
                {
                    Atom10FeedFormatter.WriteItemLastUpdatedTimeTo(writer, item.LastUpdatedTime);
                }
            }
 
            if (SerializeExtensionsAsAtom)
            {
                Atom10FeedFormatter.WriteContentTo(writer, Atom10Constants.RightsTag, item.Copyright);
            }
 
            if (!serializedContentAsDescription)
            {
                if (SerializeExtensionsAsAtom)
                {
                    Atom10FeedFormatter.WriteContentTo(writer, Atom10Constants.ContentTag, item.Content);
                }
            }
 
            if (item.Contributors.Count > 0)
            {
                if (SerializeExtensionsAsAtom)
                {
                    _atomSerializer.WriteItemContributorsTo(writer, item.Contributors);
                }
            }
 
            WriteElementExtensions(writer, item, Version);
        }
 
        private static void WriteMediaEnclosure(XmlWriter writer, SyndicationLink link, Uri baseUri)
        {
            writer.WriteStartElement(Rss20Constants.EnclosureTag, Rss20Constants.Rss20Namespace);
            Uri baseUriToWrite = FeedUtils.GetBaseUriToWrite(baseUri, link.BaseUri);
            if (baseUriToWrite != null)
            {
                writer.WriteAttributeString("xml", "base", Atom10FeedFormatter.XmlNs, FeedUtils.GetUriString(baseUriToWrite));
            }
            link.WriteAttributeExtensions(writer, SyndicationVersions.Rss20);
            if (!link.AttributeExtensions.ContainsKey(s_rss20Url))
            {
                writer.WriteAttributeString(Rss20Constants.UrlTag, Rss20Constants.Rss20Namespace, FeedUtils.GetUriString(link.Uri));
            }
            if (link.MediaType != null && !link.AttributeExtensions.ContainsKey(s_rss20Type))
            {
                writer.WriteAttributeString(Rss20Constants.TypeTag, Rss20Constants.Rss20Namespace, link.MediaType);
            }
            if (link.Length != 0 && !link.AttributeExtensions.ContainsKey(s_rss20Length))
            {
                writer.WriteAttributeString(Rss20Constants.LengthTag, Rss20Constants.Rss20Namespace, link.Length.ToString(CultureInfo.InvariantCulture));
            }
            writer.WriteEndElement();
        }
 
        private void WritePerson(XmlWriter writer, string elementTag, SyndicationPerson person)
        {
            writer.WriteStartElement(elementTag, Rss20Constants.Rss20Namespace);
            WriteAttributeExtensions(writer, person, Version);
            writer.WriteString(person.Email);
            writer.WriteEndElement();
        }
    }
 
    [XmlRoot(ElementName = Rss20Constants.RssTag, Namespace = Rss20Constants.Rss20Namespace)]
    public class Rss20FeedFormatter<TSyndicationFeed> : Rss20FeedFormatter where TSyndicationFeed : SyndicationFeed, new()
    {
        public Rss20FeedFormatter() : base(typeof(TSyndicationFeed))
        {
        }
 
        public Rss20FeedFormatter(TSyndicationFeed feedToWrite) : base(feedToWrite)
        {
        }
 
        public Rss20FeedFormatter(TSyndicationFeed feedToWrite, bool serializeExtensionsAsAtom) : base(feedToWrite, serializeExtensionsAsAtom)
        {
        }
 
        protected override SyndicationFeed CreateFeedInstance() => new TSyndicationFeed();
    }
}