#region Copyright 2010-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0 /* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #endregion using System; using System.Collections.Generic; using System.IO; using System.Xml; using System.Xml.Schema; using System.Text; using CSharpTest.Net.IO; namespace CSharpTest.Net.Html { [AttributeUsage(AttributeTargets.Field)] class DOCTYPEAttribute : Attribute { public DOCTYPEAttribute(string resource) : this(resource, String.Empty, String.Empty) { } public DOCTYPEAttribute(string resource, string Public, string System) { RESOURCE = resource; PUBLIC = Public; SYSTEM = System; } public readonly String RESOURCE; public String PUBLIC; public String SYSTEM; public XhtmlDTDSpecification DTD; } /// /// Defines the required DTD specification /// public enum XhtmlDTDSpecification { /// Use DTD only if defined None, /// /// Use the XHTML 1.0 Transitional DTD /// <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> /// [DOCTYPE("Xhtml1_0.xhtml1-strict.dtd", PUBLIC = "-/W3C/DTD XHTML 1.0 Strict/EN", SYSTEM = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd")] XhtmlStrict_10, /// /// Use the XHTML 1.0 Transitional DTD /// <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> /// [DOCTYPE("Xhtml1_0.xhtml1-transitional.dtd", PUBLIC = "-/W3C/DTD XHTML 1.0 Transitional/EN", SYSTEM = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd")] XhtmlTransitional_10, /// /// Use the XHTML 1.0 Transitional DTD /// <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"> /// [DOCTYPE("Xhtml1_0.xhtml1-frameset.dtd", PUBLIC = "-/W3C/DTD XHTML 1.0 Frameset/EN", SYSTEM = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd")] XhtmlFrameset_10, /// /// Allow any of the supported DTDs, but must be declared and compliant /// Any } /// /// Provides validation of Xhtml documents based on w3c DTDs /// public class XhtmlValidation { readonly XmlNameTable _nameTable; readonly XmlNamespaceManager _namespaces; readonly XhtmlDTDSpecification _requiresDtd; /// Creates a validator that requires documents to use any of the three DTD specifications public XhtmlValidation() : this(XhtmlDTDSpecification.Any) { } /// Creates a validator that requires documents to use the specified DTD public XhtmlValidation(XhtmlDTDSpecification dtdRequired) { _requiresDtd = dtdRequired; _nameTable = new NameTable(); _namespaces = new XmlNamespaceManager(_nameTable); _namespaces.AddNamespace("htm", "http://www.w3.org/1999/xhtml"); } /// Validate the input textreader public void Validate(string originalFilename, TextReader reader) { ValidateDocument(_requiresDtd, originalFilename, reader.ReadToEnd()); } /// Validate the input textreader public void Validate(TextReader reader) { using (TempFile temp = new TempFile()) { string content = reader.ReadToEnd(); temp.WriteAllText(content); ValidateDocument(_requiresDtd, temp.TempPath, content); } } /// Validate the input filename public void Validate(string filename) { ValidateDocument(_requiresDtd, filename, File.ReadAllText(filename)); } XmlReaderSettings MakeReaderSettings(System.Xml.XmlResolver resolver, ValidationEventHandler errorHandler) { XmlReaderSettings settings = null; settings = new XmlReaderSettings(); settings.CheckCharacters = true; settings.ConformanceLevel = ConformanceLevel.Document; settings.IgnoreComments = true; settings.NameTable = _nameTable; settings.ValidationFlags = XmlSchemaValidationFlags.ReportValidationWarnings; #pragma warning disable 612, 618 settings.ValidationType = _requiresDtd == XhtmlDTDSpecification.None ? ValidationType.Auto : ValidationType.DTD; #pragma warning restore 612, 618 settings.ValidationEventHandler += errorHandler; #if NET20 || NET35 settings.ProhibitDtd = false; #else settings.DtdProcessing = DtdProcessing.Parse; #endif settings.XmlResolver = resolver; return settings; } void ValidateDocument(XhtmlDTDSpecification expect, string filename, string contents) { XmlResolver resolver = new XmlResolver(); resolver.Credentials = null; ValidationErrorsList errors = new ValidationErrorsList(filename, contents); using (XmlReader reader = XmlReader.Create(new StringReader(contents), MakeReaderSettings(resolver, errors.OnValidationError))) { try { reader.MoveToContent(); if (expect != XhtmlDTDSpecification.None) { if (expect != XhtmlDTDSpecification.Any && expect != resolver.DTDSpecification) throw new XmlException("Missing required DTD specification.", null, 1, 1); if (reader.LocalName != "html") throw new XmlException(String.Format("Unexpected root element: {0}", reader.LocalName)); if (reader.NamespaceURI != _namespaces.LookupNamespace("htm")) throw new XmlException(String.Format("Unexpected root namespace: {0}", reader.NamespaceURI)); } while (reader.Read()) { } } catch (XmlException xmlEx) { errors.OnXmlException(xmlEx); } } errors.Assert(); } // // // class XmlResolver : System.Xml.XmlResolver { static DOCTYPEAttribute[] _docTypes; static XmlResolver() { List docTypes = new List(); foreach (System.Reflection.FieldInfo f in typeof(XhtmlDTDSpecification).GetFields()) foreach (DOCTYPEAttribute attr in f.GetCustomAttributes(typeof(DOCTYPEAttribute), false)) { attr.DTD = (XhtmlDTDSpecification)Enum.Parse(typeof(XhtmlDTDSpecification), f.Name); docTypes.Add(attr); } _docTypes = docTypes.ToArray(); } public XhtmlDTDSpecification DTDSpecification = XhtmlDTDSpecification.None; public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn) { Uri cwd = new Uri(Environment.CurrentDirectory); string loc = absoluteUri.ToString(); if (loc.StartsWith(cwd.AbsoluteUri)) loc = loc.Substring(cwd.AbsoluteUri.Length).TrimStart('/'); foreach (DOCTYPEAttribute attr in _docTypes) { if (attr.PUBLIC == loc || attr.SYSTEM == loc) { this.DTDSpecification = attr.DTD; return Check.NotNull(GetType().Assembly.GetManifestResourceStream(GetType(), attr.RESOURCE)); } } if (this.DTDSpecification == XhtmlDTDSpecification.XhtmlStrict_10 || this.DTDSpecification == XhtmlDTDSpecification.XhtmlFrameset_10 || this.DTDSpecification == XhtmlDTDSpecification.XhtmlTransitional_10) { if (loc.EndsWith("/xhtml-lat1.ent")) return Check.NotNull(GetType().Assembly.GetManifestResourceStream(GetType(), "Xhtml1_0.xhtml-lat1.ent")); else if (loc.EndsWith("/xhtml-special.ent")) return Check.NotNull(GetType().Assembly.GetManifestResourceStream(GetType(), "Xhtml1_0.xhtml-special.ent")); else if (loc.EndsWith("/xhtml-symbol.ent")) return Check.NotNull(GetType().Assembly.GetManifestResourceStream(GetType(), "Xhtml1_0.xhtml-symbol.ent")); } return null; } public override System.Net.ICredentials Credentials { set { } } } class ValidationErrorsList : List { readonly string _filename; readonly string[] _lines; XmlException _first; public ValidationErrorsList(string filename, string contents) { _first = null; _filename = filename; _lines = contents.Split('\n'); } public void Assert() { if (_first != null) { string msg = String.Join(Environment.NewLine, this.ToArray()); throw new XmlException(msg, _first, _first.LineNumber, _first.LinePosition); } } public void OnXmlException(XmlException e) { _first = _first ?? e; HandleError(e.LineNumber, e.LinePosition, XmlSeverityType.Error, e.Message); } public void OnValidationError(object sender, ValidationEventArgs e) { if (e.Exception != null) { _first = _first ?? new XmlException(e.Exception.Message, e.Exception, e.Exception.LineNumber, e.Exception.LinePosition); HandleError(e.Exception.LineNumber, e.Exception.LinePosition, e.Severity, e.Message); } } private void HandleError(int line, int pos, XmlSeverityType severity, string message) { StringBuilder errorText = new StringBuilder(); errorText.AppendFormat("{0}({1},{2}): {3}: {4}", _filename, line, pos, severity.ToString().ToLower(), message); System.Diagnostics.Trace.WriteLine(errorText.ToString()); if (line > 0 && line <= _lines.Length) { for (int ix = Math.Max(0, line - 3); ix < Math.Min(_lines.Length, line + 2); ix++) System.Diagnostics.Trace.WriteLine((ix == line ? "! " : " ") + _lines[ix]); string lineText = _lines[line - 1].TrimEnd(); if (pos > 0 && pos < lineText.Length) { if (pos > 1 && lineText[pos - 2] == '<') pos--; lineText = lineText.Substring(pos - 1); } if (lineText.Length > 0 && lineText[0] == '<') { int ixEnd = lineText.IndexOf('>'); if (ixEnd > 1 && ixEnd < lineText.Length) lineText = lineText.Substring(0, ixEnd + 1); } else lineText = lineText.Substring(0, Math.Min(lineText.Length, 120)); if (!String.IsNullOrEmpty(lineText)) { errorText.AppendLine(); errorText.Append('\t'); errorText.AppendLine(lineText); } } base.Add(errorText.ToString()); } } } }