diff options
author | Shin Mao <shmao@microsoft.com> | 2017-11-29 03:05:56 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-11-29 03:05:56 +0300 |
commit | 93764c0991d167e3b74f41f1ea129004ce10598c (patch) | |
tree | 0ef5d4ff2c76ce6c5418089a858be62876518a2a /src | |
parent | aa7165382b8ca7d3e32c7e91a72fc6496f62704b (diff) |
Improve Syndication DateTime Parsing (#25309)
* Improve Syndication DateTime Parsing.
Fix #25156
* Enable custom parser.
* Enabled StringParser.
Added tests for custom DateTimeParser and StringParser.
* Enable Custom UriParser.
Added tests for custom UriParser.
* Removed StringParser.
* Addressed CR Feedback.
* Unform LocalNames Passed to UriParser.
Diffstat (limited to 'src')
10 files changed, 554 insertions, 270 deletions
diff --git a/src/System.ServiceModel.Syndication/ref/System.ServiceModel.Syndication.cs b/src/System.ServiceModel.Syndication/ref/System.ServiceModel.Syndication.cs index e463c141e8..7103feeaa0 100644 --- a/src/System.ServiceModel.Syndication/ref/System.ServiceModel.Syndication.cs +++ b/src/System.ServiceModel.Syndication/ref/System.ServiceModel.Syndication.cs @@ -391,7 +391,9 @@ namespace System.ServiceModel.Syndication { protected SyndicationFeedFormatter() { } protected SyndicationFeedFormatter(System.ServiceModel.Syndication.SyndicationFeed feedToWrite) { } + public System.Func<string, string, string, System.DateTimeOffset> DateTimeParser { get { throw null; } set { } } public System.ServiceModel.Syndication.SyndicationFeed Feed { get { throw null; } } + public System.Func<string, System.UriKind, string, string, System.Uri> UriParser { get { throw null; } set { } } public abstract string Version { get; } public abstract bool CanRead(System.Xml.XmlReader reader); protected internal static System.ServiceModel.Syndication.SyndicationCategory CreateCategory(System.ServiceModel.Syndication.SyndicationFeed feed) { throw null; } @@ -616,4 +618,4 @@ namespace System.ServiceModel.Syndication public TContent ReadContent<TContent>(System.Xml.Serialization.XmlSerializer serializer) { throw null; } protected override void WriteContentsTo(System.Xml.XmlWriter writer) { } } -}
\ No newline at end of file +} diff --git a/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/Atom10FeedFormatter.cs b/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/Atom10FeedFormatter.cs index a01d6d074d..db52a42c56 100644 --- a/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/Atom10FeedFormatter.cs +++ b/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/Atom10FeedFormatter.cs @@ -21,7 +21,7 @@ namespace System.ServiceModel.Syndication [XmlRoot(ElementName = Atom10Constants.FeedTag, Namespace = Atom10Constants.Atom10Namespace)] public class Atom10FeedFormatter : SyndicationFeedFormatter, IXmlSerializable { - internal static readonly TimeSpan zeroOffset = new TimeSpan(0, 0, 0); + internal static readonly TimeSpan ZeroOffset = new TimeSpan(0, 0, 0); internal const string XmlNs = "http://www.w3.org/XML/1998/namespace"; internal const string XmlNsNs = "http://www.w3.org/2000/xmlns/"; private static readonly XmlQualifiedName s_atom10Href = new XmlQualifiedName(Atom10Constants.HrefTag, string.Empty); @@ -71,6 +71,11 @@ namespace System.ServiceModel.Syndication _feedType = feedToWrite.GetType(); } + internal override Func<string, string, string, DateTimeOffset> GetDefaultDateTimeParser() + { + return DateTimeHelper.DefaultAtom10DateTimeParser; + } + public bool PreserveAttributeExtensions { get { return _preserveAttributeExtensions; } @@ -304,7 +309,7 @@ namespace System.ServiceModel.Syndication } else if (reader.IsStartElement(Atom10Constants.LogoTag, Atom10Constants.Atom10Namespace)) { - result.ImageUrl = new Uri(reader.ReadElementString(), UriKind.RelativeOrAbsolute); + result.ImageUrl = UriParser(reader.ReadElementString(), UriKind.RelativeOrAbsolute, Atom10Constants.LogoTag, Atom10Constants.Atom10Namespace); } else if (reader.IsStartElement(Atom10Constants.RightsTag, Atom10Constants.Atom10Namespace)) { @@ -321,7 +326,16 @@ namespace System.ServiceModel.Syndication else if (reader.IsStartElement(Atom10Constants.UpdatedTag, Atom10Constants.Atom10Namespace)) { reader.ReadStartElement(); - result.LastUpdatedTime = DateFromString(reader.ReadString(), reader); + string dtoString = reader.ReadString(); + try + { + result.LastUpdatedTime = DateFromString(dtoString, reader); + } + catch (XmlException e) + { + result.LastUpdatedTimeException = e; + } + reader.ReadEndElement(); } else @@ -360,7 +374,16 @@ namespace System.ServiceModel.Syndication else if (reader.IsStartElement(Atom10Constants.PublishedTag, Atom10Constants.Atom10Namespace)) { reader.ReadStartElement(); - result.PublishDate = DateFromString(reader.ReadString(), reader); + string dtoString = reader.ReadString(); + try + { + result.PublishDate = DateFromString(dtoString, reader); + } + catch (XmlException e) + { + result.PublishDateException = e; + } + reader.ReadEndElement(); } else if (reader.IsStartElement(Atom10Constants.RightsTag, Atom10Constants.Atom10Namespace)) @@ -384,7 +407,16 @@ namespace System.ServiceModel.Syndication else if (reader.IsStartElement(Atom10Constants.UpdatedTag, Atom10Constants.Atom10Namespace)) { reader.ReadStartElement(); - result.LastUpdatedTime = DateFromString(reader.ReadString(), reader); + string dtoString = reader.ReadString(); + try + { + result.LastUpdatedTime = DateFromString(dtoString, reader); + } + catch (XmlException e) + { + result.LastUpdatedTimeException = e; + } + reader.ReadEndElement(); } else @@ -624,6 +656,8 @@ namespace System.ServiceModel.Syndication } } reader.MoveToElement(); + string localName = reader.LocalName; + string nameSpace = reader.NamespaceURI; string val = (kind == TextSyndicationContentKind.XHtml) ? reader.ReadInnerXml() : reader.ReadElementString(); TextSyndicationContent result = new TextSyndicationContent(val, kind); if (attrs != null) @@ -641,7 +675,7 @@ namespace System.ServiceModel.Syndication private string AsString(DateTimeOffset dateTime) { - if (dateTime.Offset == zeroOffset) + if (dateTime.Offset == ZeroOffset) { return dateTime.ToUniversalTime().ToString(Rfc3339UTCDateTimeFormat, CultureInfo.InvariantCulture); } @@ -651,44 +685,6 @@ namespace System.ServiceModel.Syndication } } - private DateTimeOffset DateFromString(string dateTimeString, XmlReader reader) - { - dateTimeString = dateTimeString.Trim(); - if (dateTimeString.Length < 20) - { - throw DiagnosticUtility.ExceptionUtility.ThrowHelperError( - new XmlException(FeedUtils.AddLineInfo(reader, - SR.ErrorParsingDateTime))); - } - if (dateTimeString[19] == '.') - { - // remove any fractional seconds, we choose to ignore them - int i = 20; - while (dateTimeString.Length > i && char.IsDigit(dateTimeString[i])) - { - ++i; - } - dateTimeString = dateTimeString.Substring(0, 19) + dateTimeString.Substring(i); - } - DateTimeOffset localTime; - if (DateTimeOffset.TryParseExact(dateTimeString, Rfc3339LocalDateTimeFormat, - CultureInfo.InvariantCulture.DateTimeFormat, - DateTimeStyles.None, out localTime)) - { - return localTime; - } - DateTimeOffset utcTime; - if (DateTimeOffset.TryParseExact(dateTimeString, Rfc3339UTCDateTimeFormat, - CultureInfo.InvariantCulture.DateTimeFormat, - DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out utcTime)) - { - return utcTime; - } - throw DiagnosticUtility.ExceptionUtility.ThrowHelperError( - new XmlException(FeedUtils.AddLineInfo(reader, - SR.ErrorParsingDateTime))); - } - private void ReadCategory(XmlReader reader, SyndicationCategory category) { ReadCategory(reader, category, this.Version, this.PreserveAttributeExtensions, this.PreserveElementExtensions, _maxExtensionSize); @@ -732,7 +728,7 @@ namespace System.ServiceModel.Syndication if (!string.IsNullOrEmpty(src)) { - result = new UrlSyndicationContent(new Uri(src, UriKind.RelativeOrAbsolute), type); + result = new UrlSyndicationContent(UriParser(src, UriKind.RelativeOrAbsolute, Atom10Constants.ContentTag, Atom10Constants.Atom10Namespace), type); bool isEmpty = reader.IsEmptyElement; if (reader.HasAttributes) { @@ -1085,7 +1081,7 @@ namespace System.ServiceModel.Syndication link.MediaType = mediaType; link.RelationshipType = relationship; link.Title = title; - link.Uri = (val != null) ? new Uri(val, UriKind.RelativeOrAbsolute) : null; + link.Uri = (val != null) ? UriParser(val, UriKind.RelativeOrAbsolute, Atom10Constants.LinkTag, Atom10Constants.Atom10Namespace) : null; } private SyndicationLink ReadLinkFrom(XmlReader reader, SyndicationFeed feed) diff --git a/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/DateTimeHelper.cs b/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/DateTimeHelper.cs new file mode 100644 index 0000000000..9ca15bf074 --- /dev/null +++ b/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/DateTimeHelper.cs @@ -0,0 +1,217 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Globalization; +using System.Text; + +namespace System.ServiceModel.Syndication +{ + internal static class DateTimeHelper + { + private const string Rfc3339DateTimeFormat = "yyyy-MM-ddTHH:mm:ssK"; + + public static DateTimeOffset DefaultRss20DateTimeParser(string dateTimeString, string localName, string ns) + { + DateTimeOffset dto; + + // First check if DateTimeOffset default parsing can parse the date + if (DateTimeOffset.TryParse(dateTimeString, out dto)) + { + return dto; + } + + // RSS specifies RFC822 + if (Rfc822DateTimeParser(dateTimeString, out dto)) + { + return dto; + } + + // Event though RCS3339 is for Atom, someone might be using this for RSS + if (Rfc3339DateTimeParser(dateTimeString, out dto)) + { + return dto; + } + + // Unable to parse - using a default date; + throw new FormatException(SR.ErrorParsingDateTime); + } + + public static DateTimeOffset DefaultAtom10DateTimeParser(string dateTimeString, string localName, string ns) + { + if (Rfc3339DateTimeParser(dateTimeString, out DateTimeOffset dto)) + { + return dto; + } + + throw new FormatException(SR.ErrorParsingDateTime); + } + + private static bool Rfc3339DateTimeParser(string dateTimeString, out DateTimeOffset dto) + { + dateTimeString = dateTimeString.Trim(); + if (dateTimeString.Length < 20) + { + return false; + } + + if (dateTimeString[19] == '.') + { + // remove any fractional seconds, we choose to ignore them + int i = 20; + while (dateTimeString.Length > i && char.IsDigit(dateTimeString[i])) + { + ++i; + } + + dateTimeString = dateTimeString.Substring(0, 19) + dateTimeString.Substring(i); + } + + return DateTimeOffset.TryParseExact(dateTimeString, Rfc3339DateTimeFormat, CultureInfo.InvariantCulture.DateTimeFormat, DateTimeStyles.None, out dto); + } + + private static bool Rfc822DateTimeParser(string dateTimeString, out DateTimeOffset dto) + { + StringBuilder dateTimeStringBuilder = new StringBuilder(dateTimeString.Trim()); + if (dateTimeStringBuilder.Length < 18) + { + return false; + } + + int timeZoneStartIndex; + for (timeZoneStartIndex = dateTimeStringBuilder.Length - 1; dateTimeStringBuilder[timeZoneStartIndex] != ' '; timeZoneStartIndex--) + ; + timeZoneStartIndex++; + + int timeZoneLength = dateTimeStringBuilder.Length - timeZoneStartIndex; + string timeZoneSuffix = dateTimeStringBuilder.ToString(timeZoneStartIndex, timeZoneLength); + dateTimeStringBuilder.Remove(timeZoneStartIndex, timeZoneLength); + bool isUtc; + dateTimeStringBuilder.Append(NormalizeTimeZone(timeZoneSuffix, out isUtc)); + string wellFormattedString = dateTimeStringBuilder.ToString(); + + DateTimeOffset theTime; + string[] parseFormat = + { + "ddd, dd MMMM yyyy HH:mm:ss zzz", + "dd MMMM yyyy HH:mm:ss zzz", + "ddd, dd MMM yyyy HH:mm:ss zzz", + "dd MMM yyyy HH:mm:ss zzz", + + "ddd, dd MMMM yyyy HH:mm zzz", + "dd MMMM yyyy HH:mm zzz", + "ddd, dd MMM yyyy HH:mm zzz", + "dd MMM yyyy HH:mm zzz", + + // The original RFC822 spec listed 2 digit years. RFC1123 updated the format to include 4 digit years and states that you should use 4 digits. + // Technically RSS2.0 specifies RFC822 but it's presumed that RFC1123 will be used as we're now past Y2K and everyone knows better. The 4 digit + // formats are listed first for performance reasons as it's presumed they will be more likely to match first. + "ddd, dd MMMM yy HH:mm:ss zzz", + "dd MMMM yyyy HH:mm:ss zzz", + "ddd, dd MMM yy HH:mm:ss zzz", + "dd MMM yyyy HH:mm:ss zzz", + + "ddd, dd MMMM yy HH:mm zzz", + "dd MMMM yyyy HH:mm zzz", + "ddd, dd MMM yy HH:mm zzz", + "dd MMM yyyy HH:mm zzz" + }; + + if (DateTimeOffset.TryParseExact(wellFormattedString, parseFormat, + CultureInfo.InvariantCulture.DateTimeFormat, + (isUtc ? DateTimeStyles.AdjustToUniversal : DateTimeStyles.None), out theTime)) + { + dto = theTime; + return true; + } + + return false; + } + + private static string NormalizeTimeZone(string rfc822TimeZone, out bool isUtc) + { + isUtc = false; + // return a string in "-08:00" format + if (rfc822TimeZone[0] == '+' || rfc822TimeZone[0] == '-') + { + // the time zone is supposed to be 4 digits but some feeds omit the initial 0 + StringBuilder result = new StringBuilder(rfc822TimeZone); + if (result.Length == 4) + { + // the timezone is +/-HMM. Convert to +/-HHMM + result.Insert(1, '0'); + } + result.Insert(3, ':'); + return result.ToString(); + } + switch (rfc822TimeZone) + { + case "UT": + case "Z": + isUtc = true; + return "-00:00"; + case "GMT": + return "-00:00"; + case "A": + return "-01:00"; + case "B": + return "-02:00"; + case "C": + return "-03:00"; + case "D": + case "EDT": + return "-04:00"; + case "E": + case "EST": + case "CDT": + return "-05:00"; + case "F": + case "CST": + case "MDT": + return "-06:00"; + case "G": + case "MST": + case "PDT": + return "-07:00"; + case "H": + case "PST": + return "-08:00"; + case "I": + return "-09:00"; + case "K": + return "-10:00"; + case "L": + return "-11:00"; + case "M": + return "-12:00"; + case "N": + return "+01:00"; + case "O": + return "+02:00"; + case "P": + return "+03:00"; + case "Q": + return "+04:00"; + case "R": + return "+05:00"; + case "S": + return "+06:00"; + case "T": + return "+07:00"; + case "U": + return "+08:00"; + case "V": + return "+09:00"; + case "W": + return "+10:00"; + case "X": + return "+11:00"; + case "Y": + return "+12:00"; + default: + return ""; + } + } + + } +} diff --git a/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/Rss20FeedFormatter.cs b/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/Rss20FeedFormatter.cs index de97d03a9d..409c24b87d 100644 --- a/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/Rss20FeedFormatter.cs +++ b/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/Rss20FeedFormatter.cs @@ -78,6 +78,11 @@ namespace System.ServiceModel.Syndication _feedType = feedToWrite.GetType(); } + internal override Func<string, string, string, DateTimeOffset> GetDefaultDateTimeParser() + { + return DateTimeHelper.DefaultRss20DateTimeParser; + } + public bool PreserveAttributeExtensions { get { return _preserveAttributeExtensions; } @@ -254,211 +259,10 @@ namespace System.ServiceModel.Syndication this.WriteItem(writer, item, feedBaseUri); } } - - private static DateTimeOffset DateFromString(string dateTimeString, XmlReader reader) - { - StringBuilder dateTimeStringBuilder = new StringBuilder(dateTimeString.Trim()); - if (dateTimeStringBuilder.Length < 18) - { - throw DiagnosticUtility.ExceptionUtility.ThrowHelperError( - new XmlException(FeedUtils.AddLineInfo(reader, - SR.ErrorParsingDateTime))); - } - if (dateTimeStringBuilder[3] == ',') - { - // There is a leading (e.g.) "Tue, ", strip it off - dateTimeStringBuilder.Remove(0, 4); - // There's supposed to be a space here but some implementations dont have one - RemoveExtraWhiteSpaceAtStart(dateTimeStringBuilder); - } - ReplaceMultipleWhiteSpaceWithSingleWhiteSpace(dateTimeStringBuilder); - if (char.IsDigit(dateTimeStringBuilder[1])) - { - // two-digit day, we are good - } - else - { - dateTimeStringBuilder.Insert(0, '0'); - } - if (dateTimeStringBuilder.Length < 19) - { - throw DiagnosticUtility.ExceptionUtility.ThrowHelperError( - new XmlException(FeedUtils.AddLineInfo(reader, - SR.ErrorParsingDateTime))); - } - bool thereAreSeconds = (dateTimeStringBuilder[17] == ':'); - int timeZoneStartIndex; - if (thereAreSeconds) - { - timeZoneStartIndex = 21; - } - else - { - timeZoneStartIndex = 18; - } - string timeZoneSuffix = dateTimeStringBuilder.ToString().Substring(timeZoneStartIndex); - dateTimeStringBuilder.Remove(timeZoneStartIndex, dateTimeStringBuilder.Length - timeZoneStartIndex); - bool isUtc; - dateTimeStringBuilder.Append(NormalizeTimeZone(timeZoneSuffix, out isUtc)); - string wellFormattedString = dateTimeStringBuilder.ToString(); - - DateTimeOffset theTime; - string parseFormat; - if (thereAreSeconds) - { - parseFormat = "dd MMM yyyy HH:mm:ss zzz"; - } - else - { - parseFormat = "dd MMM yyyy HH:mm zzz"; - } - if (DateTimeOffset.TryParseExact(wellFormattedString, parseFormat, - CultureInfo.InvariantCulture.DateTimeFormat, - (isUtc ? DateTimeStyles.AdjustToUniversal : DateTimeStyles.None), out theTime)) - { - return theTime; - } - throw DiagnosticUtility.ExceptionUtility.ThrowHelperError( - new XmlException(FeedUtils.AddLineInfo(reader, - SR.ErrorParsingDateTime))); - } - - private static string NormalizeTimeZone(string rfc822TimeZone, out bool isUtc) - { - isUtc = false; - // return a string in "-08:00" format - if (rfc822TimeZone[0] == '+' || rfc822TimeZone[0] == '-') - { - // the time zone is supposed to be 4 digits but some feeds omit the initial 0 - StringBuilder result = new StringBuilder(rfc822TimeZone); - if (result.Length == 4) - { - // the timezone is +/-HMM. Convert to +/-HHMM - result.Insert(1, '0'); - } - result.Insert(3, ':'); - return result.ToString(); - } - switch (rfc822TimeZone) - { - case "UT": - case "Z": - isUtc = true; - return "-00:00"; - case "GMT": - return "-00:00"; - case "A": - return "-01:00"; - case "B": - return "-02:00"; - case "C": - return "-03:00"; - case "D": - case "EDT": - return "-04:00"; - case "E": - case "EST": - case "CDT": - return "-05:00"; - case "F": - case "CST": - case "MDT": - return "-06:00"; - case "G": - case "MST": - case "PDT": - return "-07:00"; - case "H": - case "PST": - return "-08:00"; - case "I": - return "-09:00"; - case "K": - return "-10:00"; - case "L": - return "-11:00"; - case "M": - return "-12:00"; - case "N": - return "+01:00"; - case "O": - return "+02:00"; - case "P": - return "+03:00"; - case "Q": - return "+04:00"; - case "R": - return "+05:00"; - case "S": - return "+06:00"; - case "T": - return "+07:00"; - case "U": - return "+08:00"; - case "V": - return "+09:00"; - case "W": - return "+10:00"; - case "X": - return "+11:00"; - case "Y": - return "+12:00"; - default: - return ""; - } - } - - private static void RemoveExtraWhiteSpaceAtStart(StringBuilder stringBuilder) - { - int i = 0; - while (i < stringBuilder.Length) - { - if (!char.IsWhiteSpace(stringBuilder[i])) - { - break; - } - ++i; - } - if (i > 0) - { - stringBuilder.Remove(0, i); - } - } - - private static void ReplaceMultipleWhiteSpaceWithSingleWhiteSpace(StringBuilder builder) - { - int index = 0; - int whiteSpaceStart = -1; - while (index < builder.Length) - { - if (char.IsWhiteSpace(builder[index])) - { - if (whiteSpaceStart < 0) - { - whiteSpaceStart = index; - // normalize all white spaces to be ' ' so that the date time parsing works - builder[index] = ' '; - } - } - else if (whiteSpaceStart >= 0) - { - if (index > whiteSpaceStart + 1) - { - // there are at least 2 spaces... replace by 1 - builder.Remove(whiteSpaceStart, index - whiteSpaceStart - 1); - index = whiteSpaceStart + 1; - } - whiteSpaceStart = -1; - } - ++index; - } - // we have already trimmed the start and end so there cannot be a trail of white spaces in the end - Debug.Assert(builder.Length == 0 || builder[builder.Length - 1] != ' ', "The string builder doesnt end in a white space"); - } - + private string AsString(DateTimeOffset dateTime) { - if (dateTime.Offset == Atom10FeedFormatter.zeroOffset) + if (dateTime.Offset == Atom10FeedFormatter.ZeroOffset) { return dateTime.ToUniversalTime().ToString(Rfc822OutputUtcDateTimeFormat, CultureInfo.InvariantCulture); } @@ -497,8 +301,9 @@ namespace System.ServiceModel.Syndication } } } + string uri = reader.ReadElementString(); - link.Uri = new Uri(uri, UriKind.RelativeOrAbsolute); + link.Uri = UriParser(uri, UriKind.RelativeOrAbsolute, Rss20Constants.LinkTag, Rss20Constants.Rss20Namespace); return link; } @@ -601,6 +406,8 @@ namespace System.ServiceModel.Syndication if (!isEmpty) { string fallbackAlternateLink = null; + string fallbackAlternateLinkLocalName = null; + string fallbackAlternateLinkNamespace = null; XmlDictionaryWriter extWriter = null; bool readAlternateLink = false; try @@ -641,10 +448,13 @@ namespace System.ServiceModel.Syndication { 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)) @@ -656,7 +466,14 @@ namespace System.ServiceModel.Syndication string str = reader.ReadString(); if (!string.IsNullOrEmpty(str)) { - result.PublishDate = DateFromString(str, reader); + try + { + result.PublishDate = DateFromString(str, reader); + } + catch (XmlException e) + { + result.PublishDateException = e; + } } reader.ReadEndElement(); } @@ -677,7 +494,7 @@ namespace System.ServiceModel.Syndication string val = reader.Value; if (name == Rss20Constants.UrlTag && ns == Rss20Constants.Rss20Namespace) { - feed.Links.Add(SyndicationLink.CreateSelfLink(new Uri(val, UriKind.RelativeOrAbsolute))); + feed.Links.Add(SyndicationLink.CreateSelfLink(UriParser(val, UriKind.RelativeOrAbsolute, Rss20Constants.UrlTag, Rss20Constants.Rss20Namespace))); } else if (!FeedUtils.IsXmlns(name, ns)) { @@ -692,6 +509,7 @@ namespace System.ServiceModel.Syndication } } } + string feedTitle = reader.ReadElementString(); feed.Title = new TextSyndicationContent(feedTitle); result.SourceFeed = feed; @@ -729,7 +547,7 @@ namespace System.ServiceModel.Syndication reader.ReadEndElement(); // item if (!readAlternateLink && fallbackAlternateLink != null) { - result.Links.Add(SyndicationLink.CreateAlternateLink(new Uri(fallbackAlternateLink, UriKind.RelativeOrAbsolute))); + result.Links.Add(SyndicationLink.CreateAlternateLink(UriParser(fallbackAlternateLink, UriKind.RelativeOrAbsolute, fallbackAlternateLinkLocalName, fallbackAlternateLinkNamespace))); readAlternateLink = true; } @@ -775,7 +593,7 @@ namespace System.ServiceModel.Syndication string val = reader.Value; if (name == Rss20Constants.UrlTag && ns == Rss20Constants.Rss20Namespace) { - link.Uri = new Uri(val, UriKind.RelativeOrAbsolute); + link.Uri = UriParser(val, UriKind.RelativeOrAbsolute, Rss20Constants.EnclosureTag, Rss20Constants.Rss20Namespace); } else if (name == Rss20Constants.TypeTag && ns == Rss20Constants.Rss20Namespace) { @@ -861,7 +679,11 @@ namespace System.ServiceModel.Syndication 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) { @@ -873,10 +695,14 @@ namespace System.ServiceModel.Syndication 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()) @@ -886,6 +712,8 @@ namespace System.ServiceModel.Syndication if (name == "base" && ns == Atom10FeedFormatter.XmlNs) { baseUri = reader.Value; + baseUriLocalName = elementLocalName; + baseUriNamespace = elementNamespace; continue; } if (FeedUtils.IsXmlns(name, ns) || FeedUtils.IsXmlSchemaType(name, ns)) @@ -906,10 +734,12 @@ namespace System.ServiceModel.Syndication } } } + if (!string.IsNullOrEmpty(baseUri)) { - result.BaseUri = new Uri(baseUri, UriKind.RelativeOrAbsolute); + result.BaseUri = UriParser(baseUri, UriKind.RelativeOrAbsolute, baseUriLocalName, baseUriNamespace); } + bool areAllItemsRead = true; reader.ReadStartElement(Rss20Constants.ChannelTag, Rss20Constants.Rss20Namespace); @@ -954,7 +784,14 @@ namespace System.ServiceModel.Syndication string str = reader.ReadString(); if (!string.IsNullOrEmpty(str)) { - result.LastUpdatedTime = DateFromString(str, reader); + try + { + result.LastUpdatedTime = DateFromString(str, reader); + } + catch (XmlException e) + { + result.LastUpdatedTimeException = e; + } } reader.ReadEndElement(); } @@ -974,7 +811,7 @@ namespace System.ServiceModel.Syndication { if (reader.IsStartElement(Rss20Constants.UrlTag, Rss20Constants.Rss20Namespace)) { - result.ImageUrl = new Uri(reader.ReadElementString(), UriKind.RelativeOrAbsolute); + result.ImageUrl = UriParser(reader.ReadElementString(), UriKind.RelativeOrAbsolute, Rss20Constants.UrlTag, Rss20Constants.Rss20Namespace); } else { diff --git a/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/SyndicationFeed.cs b/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/SyndicationFeed.cs index 6df7450bd7..621c98994f 100644 --- a/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/SyndicationFeed.cs +++ b/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/SyndicationFeed.cs @@ -227,10 +227,24 @@ namespace System.ServiceModel.Syndication set { _language = value; } } + internal Exception LastUpdatedTimeException { get; set; } + public DateTimeOffset LastUpdatedTime { - get { return _lastUpdatedTime; } - set { _lastUpdatedTime = value; } + get + { + if (LastUpdatedTimeException != null) + { + throw LastUpdatedTimeException; + } + + return _lastUpdatedTime; + } + set + { + LastUpdatedTimeException = null; + _lastUpdatedTime = value; + } } public Collection<SyndicationLink> Links diff --git a/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/SyndicationFeedFormatter.cs b/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/SyndicationFeedFormatter.cs index fa89a8493b..750563c606 100644 --- a/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/SyndicationFeedFormatter.cs +++ b/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/SyndicationFeedFormatter.cs @@ -7,11 +7,9 @@ namespace System.ServiceModel.Syndication using System; using System.Diagnostics; using System.Globalization; - using System.Runtime; using System.Runtime.Serialization; using System.Xml; using DiagnosticUtility = System.ServiceModel.DiagnosticUtility; - using System.Runtime.CompilerServices; [DataContract] public abstract class SyndicationFeedFormatter @@ -21,6 +19,7 @@ namespace System.ServiceModel.Syndication protected SyndicationFeedFormatter() { _feed = null; + DateTimeParser = GetDefaultDateTimeParser(); } protected SyndicationFeedFormatter(SyndicationFeed feedToWrite) @@ -30,6 +29,7 @@ namespace System.ServiceModel.Syndication throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("feedToWrite"); } _feed = feedToWrite; + DateTimeParser = GetDefaultDateTimeParser(); } public SyndicationFeed Feed @@ -40,6 +40,21 @@ namespace System.ServiceModel.Syndication } } + public Func<string, UriKind, string, string, Uri> UriParser { get; set; } = DefaultUriParser; + + // Different DateTimeParsers are needed for Atom and Rss so can't set inline + public Func<string, string, string, DateTimeOffset> DateTimeParser { get; set; } + + internal virtual Func<string, string, string, DateTimeOffset> GetDefaultDateTimeParser() + { + return NotImplementedDateTimeParser; + } + + private DateTimeOffset NotImplementedDateTimeParser(string dtoString, string localName, string ns) + { + throw new NotImplementedException(); + } + public abstract string Version { get; } @@ -376,6 +391,24 @@ namespace System.ServiceModel.Syndication _feed = feed; } + internal DateTimeOffset DateFromString(string dateTimeString, XmlReader reader) + { + try + { + return DateTimeParser(dateTimeString, reader.LocalName, reader.NamespaceURI); + } + catch (FormatException e) + { + throw DiagnosticUtility.ExceptionUtility.ThrowHelperError( + new XmlException(FeedUtils.AddLineInfo(reader, SR.ErrorParsingDateTime), e)); + } + } + + private static Uri DefaultUriParser(string value, UriKind kind, string localName, string ns) + { + return new Uri(value, kind); + } + internal static void CloseBuffer(XmlBuffer buffer, XmlDictionaryWriter extWriter) { if (buffer == null) diff --git a/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/SyndicationItem.cs b/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/SyndicationItem.cs index 3f8648be02..08cf46b42e 100644 --- a/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/SyndicationItem.cs +++ b/src/System.ServiceModel.Syndication/src/System/ServiceModel/Syndication/SyndicationItem.cs @@ -158,10 +158,24 @@ namespace System.ServiceModel.Syndication set { _id = value; } } + internal Exception LastUpdatedTimeException { get; set; } + public DateTimeOffset LastUpdatedTime { - get { return _lastUpdatedTime; } - set { _lastUpdatedTime = value; } + get + { + if (LastUpdatedTimeException != null) + { + throw LastUpdatedTimeException; + } + + return _lastUpdatedTime; + } + set + { + LastUpdatedTimeException = null; + _lastUpdatedTime = value; + } } public Collection<SyndicationLink> Links @@ -176,10 +190,24 @@ namespace System.ServiceModel.Syndication } } + internal Exception PublishDateException { get; set; } + public DateTimeOffset PublishDate { - get { return _publishDate; } - set { _publishDate = value; } + get + { + if (PublishDateException != null) + { + throw PublishDateException; + } + + return _publishDate; + } + set + { + PublishDateException = null; + _publishDate = value; + } } public SyndicationFeed SourceFeed diff --git a/src/System.ServiceModel.Syndication/tests/BasicScenarioTests.cs b/src/System.ServiceModel.Syndication/tests/BasicScenarioTests.cs index ef2665785e..ee89b27219 100644 --- a/src/System.ServiceModel.Syndication/tests/BasicScenarioTests.cs +++ b/src/System.ServiceModel.Syndication/tests/BasicScenarioTests.cs @@ -9,6 +9,7 @@ using System.ServiceModel.Syndication; using System.Xml; using System.IO; using Xunit; +using System.Linq; namespace System.ServiceModel.Syndication.Tests { @@ -285,19 +286,126 @@ namespace System.ServiceModel.Syndication.Tests } [Fact] - [ActiveIssue(25156)] public static void SyndicationFeed_Rss_WrongDateFormat() { // *** SETUP *** \\ - Rss20FeedFormatter rssformatter = new Rss20FeedFormatter(); - XmlReader reader = XmlReader.Create(@"rssSpecExampleWrongDateFormat.xml"); // *** EXECUTE *** \\ SyndicationFeed res = SyndicationFeed.Load(reader); // *** ASSERT *** \\ - Assert.True(!res.LastUpdatedTime.Equals(new DateTimeOffset())); + Assert.True(res != null, "res was null."); + Assert.Equal(new DateTimeOffset(2016, 8, 23, 16, 8, 0, new TimeSpan(-4, 0, 0)), res.LastUpdatedTime); + Assert.True(res.Items != null, "res.Items was null."); + Assert.True(res.Items.Count() == 4, $"res.Items.Count() was not as expected. Expected: 4; Actual: {res.Items.Count()}"); + SyndicationItem[] items = res.Items.ToArray(); + DateTimeOffset dateTimeOffset; + Assert.Throws<XmlException>(() => dateTimeOffset = items[2].PublishDate); + } + + [Fact] + public static void SyndicationFeed_Rss_DateTimeParser() + { + // *** SETUP *** \\ + // *** EXECUTE *** \\ + SyndicationFeed feed; + DateTimeOffset dto = new DateTimeOffset(2017, 1, 2, 3, 4, 5, new TimeSpan(0)); + using (XmlReader reader = XmlReader.Create(@"RssSpecCustomParser.xml")) + { + var formatter = new Rss20FeedFormatter(); + formatter.DateTimeParser = (value, localName, ns) => dto; + formatter.ReadFrom(reader); + feed = formatter.Feed; + } + + // *** ASSERT *** \\ + Assert.True(feed != null, "res was null."); + Assert.Equal(dto, feed.LastUpdatedTime); + } + + [Fact] + public static void SyndicationFeed_Rss_UriParser() + { + // *** SETUP *** \\ + // *** EXECUTE *** \\ + SyndicationFeed feed; + using (XmlReader reader = XmlReader.Create(@"RssSpecCustomParser.xml")) + { + var formatter = new Rss20FeedFormatter + { + UriParser = (value, kind, localName, ns) => new Uri($"http://value-{value}-kind-{kind}-localName-{localName}-ns-{ns}-end") + }; + formatter.ReadFrom(reader); + feed = formatter.Feed; + } + + // *** ASSERT *** \\ + Assert.True(feed != null, "res was null."); + Assert.Equal(new Uri("http://value-ChannelBase-kind-relativeorabsolute-localName-channel-ns--end"), feed.BaseUri); + Assert.Equal(new Uri("http://value-ImageUrl-kind-relativeorabsolute-localName-url-ns--end"), feed.ImageUrl); + Assert.NotNull(feed.Links); + Assert.Equal(1, feed.Links.Count); + Assert.Equal(new Uri("http://value-FeedLink-kind-relativeorabsolute-localName-link-ns--end"), feed.Links.First().Uri); + + Assert.True(feed.Items != null, "res.Items was null."); + Assert.Equal(1, feed.Items.Count()); + Assert.Equal(1, feed.Items.First().Links.Count); + Assert.Equal(new Uri("http://value-itemlink-kind-relativeorabsolute-localName-link-ns--end"), feed.Items.First().Links.First().Uri); + } + + [Fact] + public static void SyndicationFeed_Atom_DateTimeParser() + { + // *** SETUP *** \\ + // *** EXECUTE *** \\ + SyndicationFeed feed; + DateTimeOffset dto = new DateTimeOffset(2017, 1, 2, 3, 4, 5, new TimeSpan(0)); + using (XmlReader reader = XmlReader.Create(@"SimpleAtomFeedCustomParser.xml")) + { + var formatter = new Atom10FeedFormatter + { + DateTimeParser = (value, localName, ns) => dto + }; + formatter.ReadFrom(reader); + feed = formatter.Feed; + } + + // *** ASSERT *** \\ + Assert.True(feed != null, "res was null."); + Assert.Equal(dto, feed.LastUpdatedTime); + + Assert.True(feed.Items != null, "res.Items was null."); + Assert.Equal(1, feed.Items.Count()); + Assert.Equal(dto, feed.Items.First().LastUpdatedTime); + } + + [Fact] + public static void SyndicationFeed_Atom_UriParser() + { + // *** SETUP *** \\ + // *** EXECUTE *** \\ + SyndicationFeed feed; + using (XmlReader reader = XmlReader.Create(@"SimpleAtomFeedCustomParser.xml")) + { + var formatter = new Atom10FeedFormatter + { + UriParser = (value, kind, localName, ns) => new Uri($"http://value-{value}-kind-{kind}-localName-{localName}-ns-{ns}-end") + }; + formatter.ReadFrom(reader); + feed = formatter.Feed; + } + + // *** ASSERT *** \\ + Assert.True(feed != null, "res was null."); + Assert.Equal(new Uri("http://value-FeedLogo-kind-relativeorabsolute-localName-logo-ns-http//www.w3.org/2005/Atom-end"), feed.ImageUrl); + + Assert.True(feed.Items != null, "res.Items was null."); + Assert.Equal(1, feed.Items.Count()); + Assert.NotNull(feed.Items.First().Links); + Assert.Equal(1, feed.Items.First().Links.Count); + Assert.Equal(new Uri("http://value-EntryLinkHref-kind-relativeorabsolute-localName-link-ns-http//www.w3.org/2005/Atom-end"), feed.Items.First().Links.First().Uri); + Assert.Equal(new Uri("http://value-EntryContentSrc-kind-relativeorabsolute-localName-content-ns-http://www.w3.org/2005/Atom-end"), ((UrlSyndicationContent)feed.Items.First().Content).Url); } [Fact] diff --git a/src/System.ServiceModel.Syndication/tests/TestFeeds/RssSpecCustomParser.xml b/src/System.ServiceModel.Syndication/tests/TestFeeds/RssSpecCustomParser.xml new file mode 100644 index 0000000000..c90436a4f7 --- /dev/null +++ b/src/System.ServiceModel.Syndication/tests/TestFeeds/RssSpecCustomParser.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<rss version="2.0"> + <channel xml:base="ChannelBase" customAttribute="customAtt"> + <title>FeedTitle</title> + <link>FeedLink</link> + <description>FeedDescription</description> + <language>FeedLanguage</language> + <lastBuildDate>Tue, 23 August 2016 16:08:00 EDT</lastBuildDate> + <docs>http://blogs.law.harvard.edu/tech/rss</docs> + <generator>FeedGenerator</generator> + <managingEditor>FeedEditorEmail</managingEditor> + <category>FeedCategory</category> + <copyright>FeedCopyright</copyright> + <ttl>60</ttl> + <image> + <url>ImageUrl</url> + </image> + <item> + <title>ItemTitle</title> + <link>ItemLink</link> + <description>ItemDescription</description> + <pubDate>Tue, 03 Jun 2003 09:39:21 GMT</pubDate> + <guid>ItemGuid</guid> + </item> + </channel> +</rss>
\ No newline at end of file diff --git a/src/System.ServiceModel.Syndication/tests/TestFeeds/SimpleAtomFeedCustomParser.xml b/src/System.ServiceModel.Syndication/tests/TestFeeds/SimpleAtomFeedCustomParser.xml new file mode 100644 index 0000000000..e6237940dc --- /dev/null +++ b/src/System.ServiceModel.Syndication/tests/TestFeeds/SimpleAtomFeedCustomParser.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<feed xml:base="http://mypage.com/" xmlns="http://www.w3.org/2005/Atom"> + <title type="text">FeedTitle</title> + <subtitle type="text">FeedSubtitle</subtitle> + <id>FeedID</id> + <updated>2017-06-26T14:41:43-07:00</updated> + <logo>FeedLogo</logo> + <generator>FeedGenerator</generator> + <author> + <name>AuthorName</name> + <email>author@Contoso.com</email> + <uri>AuthorUri</uri> + </author> + <link rel="alternate" href="http://www.contoso.com/news" /> + <CustomElement xmlns="">asd</CustomElement> + <entry> + <id>EntryId</id> + <title type="text">SyndicationFeed released for .net Core</title> + <updated>2017-06-26T21:41:43Z</updated> + <link rel="alternate" href="EntryLinkHref" /> + <content src="EntryContentSrc" /> + </entry> +</feed>
\ No newline at end of file |