//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Configuration { using System.Configuration.Internal; using System.Collections; using System.Collections.Specialized; using System.Collections.Generic; using System.Configuration; using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Security; using System.Security.Permissions; using System.Text; using System.Xml; using System.Net; // // XmlTextReader Helper class. // // Provides the following services: // // * Reader methods that verify restrictions on the XML that can be contained in a config file. // * Methods to copy the reader stream to a writer stream. // * Method to copy a configuration section to a string. // * Methods to format a string of XML. // // Errors found during read are accumlated in a ConfigurationSchemaErrors object. // internal sealed class XmlUtil : IDisposable, IConfigErrorInfo { private const int MAX_LINE_WIDTH=60; // Offset from where the reader reports the LinePosition of an Xml Node to // the start of that representation in text. static readonly int[] s_positionOffset = { 0, // None, 1, // Element, -1, // EndEntity, N/A 2, // XmlDeclaration 0, "trueLinePosition > 0"); return trueLinePosition; } } internal XmlTextReader Reader { get { return _reader; } } internal ConfigurationSchemaErrors SchemaErrors { get { return _schemaErrors; } } // // Read until the Next Element element, or we hit // the end of the file. // internal void ReadToNextElement() { while (_reader.Read()) { if (_reader.MoveToContent() == XmlNodeType.Element) { // We found an element, so return return; } } // We must of hit end of file } // // Skip this element and its children, then read to next start element, // or until we hit end of file. // internal void SkipToNextElement() { _reader.Skip(); _reader.MoveToContent(); while (!_reader.EOF && _reader.NodeType != XmlNodeType.Element) { _reader.Read(); _reader.MoveToContent(); } } // // Read to the next start element, and verify that all XML nodes read are permissible. // internal void StrictReadToNextElement(ExceptionAction action) { while (_reader.Read()) { // optimize for the common case if (_reader.NodeType == XmlNodeType.Element) { return; } VerifyIgnorableNodeType(action); } } // // Skip this element and its children, then read to next start element, // or until we hit end of file. Verify that nodes that are read after the // skipped element are permissible. // internal void StrictSkipToNextElement(ExceptionAction action) { _reader.Skip(); while (!_reader.EOF && _reader.NodeType != XmlNodeType.Element) { VerifyIgnorableNodeType(action); _reader.Read(); } } // // StrictSkipToOurParentsEndElement // // Skip until we hit the end element for our parent, and verify // that nodes at the parent level are permissible. // internal void StrictSkipToOurParentsEndElement(ExceptionAction action) { int currentDepth = _reader.Depth; // Skip everything at out current level while (_reader.Depth >= currentDepth) { _reader.Skip(); } while (!_reader.EOF && _reader.NodeType != XmlNodeType.EndElement) { VerifyIgnorableNodeType(action); _reader.Read(); } } // // Add an error if the node type is not permitted by the configuration schema. // internal void VerifyIgnorableNodeType(ExceptionAction action) { XmlNodeType nodeType = _reader.NodeType; if (nodeType != XmlNodeType.Comment && nodeType != XmlNodeType.EndElement) { ConfigurationException ex = new ConfigurationErrorsException( SR.GetString(SR.Config_base_unrecognized_element), this); SchemaErrors.AddError(ex, action); } } // // Add an error if there are attributes that have not been examined, // and are therefore unrecognized. // internal void VerifyNoUnrecognizedAttributes(ExceptionAction action) { if (_reader.MoveToNextAttribute()) { AddErrorUnrecognizedAttribute(action); } } // // Add an error if the retrieved attribute is null, // and therefore not present. // internal bool VerifyRequiredAttribute( object o, string attrName, ExceptionAction action) { if (o == null) { AddErrorRequiredAttribute(attrName, action); return false; } else { return true; } } // // Functions to handle parsing errors // internal void AddErrorUnrecognizedAttribute(ExceptionAction action) { ConfigurationErrorsException ex = new ConfigurationErrorsException( SR.GetString(SR.Config_base_unrecognized_attribute, _reader.Name), this); SchemaErrors.AddError(ex, action); } internal void AddErrorRequiredAttribute(string attrib, ExceptionAction action) { ConfigurationErrorsException ex = new ConfigurationErrorsException( SR.GetString(SR.Config_missing_required_attribute, attrib, _reader.Name), this); SchemaErrors.AddError(ex, action); } internal void AddErrorReservedAttribute(ExceptionAction action) { ConfigurationErrorsException ex = new ConfigurationErrorsException( SR.GetString(SR.Config_reserved_attribute, _reader.Name), this); SchemaErrors.AddError(ex, action); } internal void AddErrorUnrecognizedElement(ExceptionAction action) { ConfigurationErrorsException ex = new ConfigurationErrorsException( SR.GetString(SR.Config_base_unrecognized_element), this); SchemaErrors.AddError(ex, action); } internal void VerifyAndGetNonEmptyStringAttribute(ExceptionAction action, out string newValue) { if (!String.IsNullOrEmpty(_reader.Value)) { newValue = _reader.Value; } else { newValue = null; ConfigurationException ex = new ConfigurationErrorsException( SR.GetString(SR.Empty_attribute, _reader.Name), this); SchemaErrors.AddError(ex, action); } } // VerifyAndGetBooleanAttribute // // Verify and Retrieve the Boolean Attribute. If it is not // a valid value then log an error and set the value to a given default. // internal void VerifyAndGetBooleanAttribute( ExceptionAction action, bool defaultValue, out bool newValue) { if (_reader.Value == "true") { newValue = true; } else if (_reader.Value == "false") { newValue = false; } else { // Unrecognized value newValue = defaultValue; ConfigurationErrorsException ex = new ConfigurationErrorsException( SR.GetString(SR.Config_invalid_boolean_attribute, _reader.Name), this); SchemaErrors.AddError(ex, action); } } // // Copy an XML element, then continue copying until we've hit the next element // or exited this depth. // internal bool CopyOuterXmlToNextElement(XmlUtilWriter utilWriter, bool limitDepth) { CopyElement(utilWriter); // Copy until reaching the next element, or if limitDepth == true until we've exited this depth. return CopyReaderToNextElement(utilWriter, limitDepth); } // // Copy an XML element but skip all its child elements, then continue copying until we've hit the next element. // internal bool SkipChildElementsAndCopyOuterXmlToNextElement(XmlUtilWriter utilWriter) { bool isEmptyElement = _reader.IsEmptyElement; int startingLine = _reader.LineNumber; #if DBG int depth = _reader.Depth; #endif Debug.Assert(_reader.NodeType == XmlNodeType.Element, "_reader.NodeType == XmlNodeType.Element"); CopyXmlNode(utilWriter); // See if we need to skip any child element if (!isEmptyElement) { while (_reader.NodeType != XmlNodeType.EndElement) { // Skip all the inner child elements if (_reader.NodeType == XmlNodeType.Element) { _reader.Skip(); // We need to skip all the whitespaces following a skipped element. // - If the whitespaces don't contain /r/n, then it's okay to skip them // as part of the element. // - If the whitespaces contain /r/n, not skipping them will result // in a redundant emtpy line being copied. if (_reader.NodeType == XmlNodeType.Whitespace) { _reader.Skip(); } } else { // We want to preserve other content, e.g. comments. CopyXmlNode(utilWriter); } } if (_reader.LineNumber != startingLine) { // The whitespace in front of the EndElement was skipped above. // We need to append spaces to compensate for that. utilWriter.AppendSpacesToLinePosition(TrueLinePosition); } #if DBG Debug.Assert(_reader.Depth == depth, "We should be at the same depth as the opening Element"); #endif // Copy the end element. CopyXmlNode(utilWriter); } return CopyReaderToNextElement(utilWriter, true); } // // Copy the reader until we hit an element, or we've exited the current depth. // internal bool CopyReaderToNextElement(XmlUtilWriter utilWriter, bool limitDepth) { bool moreToRead = true; // Set the depth if we limit copying to this depth int depth; if (limitDepth) { // there is nothing in the element if (_reader.NodeType == XmlNodeType.EndElement) return true; depth = _reader.Depth; } else { depth = 0; } // Copy nodes until we've reached the desired depth, or until we hit an element. do { if (_reader.NodeType == XmlNodeType.Element) break; if (_reader.Depth < depth) { break; } moreToRead = CopyXmlNode(utilWriter); } while (moreToRead); return moreToRead; } // // Skip over the current element and copy until the next element. // This function removes the one blank line that would otherwise // be inserted by simply skipping and copying to the next element // in a situation like this: // // // // // // // // // internal bool SkipAndCopyReaderToNextElement(XmlUtilWriter utilWriter, bool limitDepth) { Debug.Assert(_reader.NodeType == XmlNodeType.Element, "_reader.NodeType == XmlNodeType.Element"); // If the last line before the element is not blank, then we do not have to // remove the blank line. if (!utilWriter.IsLastLineBlank) { _reader.Skip(); return CopyReaderToNextElement(utilWriter, limitDepth); } // Set the depth if we limit copying to this depth int depth; if (limitDepth) { depth = _reader.Depth; } else { depth = 0; } // Skip over the element _reader.Skip(); int lineNumberOfEndElement = _reader.LineNumber; // Read until we hit a a non-whitespace node or reach the end while (!_reader.EOF) { if (_reader.NodeType != XmlNodeType.Whitespace) { // // If the next non-whitepace node is on another line, // seek back to the beginning of the current blank line, // skip a blank line of whitespace, and copy the remaining whitespace. // if (_reader.LineNumber > lineNumberOfEndElement) { utilWriter.SeekToLineStart(); utilWriter.AppendWhiteSpace(lineNumberOfEndElement + 1, 1, LineNumber, TrueLinePosition); } break; } _reader.Read(); } // Copy nodes until we've reached the desired depth, or until we hit an element. while (!_reader.EOF) { if (_reader.NodeType == XmlNodeType.Element) break; if (_reader.Depth < depth) { break; } CopyXmlNode(utilWriter); }; return !_reader.EOF; } // // Copy an XML element and its children, up to and including the end element. // private void CopyElement(XmlUtilWriter utilWriter) { Debug.Assert(_reader.NodeType== XmlNodeType.Element, "_reader.NodeType== XmlNodeType.Element"); int depth = _reader.Depth; bool isEmptyElement = _reader.IsEmptyElement; // Copy current node CopyXmlNode(utilWriter); // Copy nodes while the depth is greater than the current depth. while (_reader.Depth > depth) { CopyXmlNode(utilWriter); } // Copy the end element. if (!isEmptyElement) { CopyXmlNode(utilWriter); } } // // Copy a single XML node, attempting to preserve whitespace. // A side effect of this method is to advance the reader to the next node. // // PERFORMANCE NOTE: this function is used at runtime to copy a configuration section, // and at designtime to copy an entire XML document. // // At designtime, this function needs to be able to copy a " // the XmlReader API does not give us the location of the closing string, e.g. ">". // To correctly determine the location of the closing part, we advance the reader, // determine the position of the next node, then work backwards to add whitespace // and add the closing string. // string close = null; int lineNumber = -1; int linePosition = -1; int readerLineNumber = 0; int readerLinePosition = 0; int writerLineNumber = 0; int writerLinePosition = 0; if (utilWriter.TrackPosition) { readerLineNumber = _reader.LineNumber; readerLinePosition = _reader.LinePosition; writerLineNumber = utilWriter.LineNumber; writerLinePosition = utilWriter.LinePosition; } // We test the node type in the likely order of decreasing occurrence. XmlNodeType nodeType = _reader.NodeType; if (nodeType == XmlNodeType.Whitespace) { utilWriter.Write(_reader.Value); } else if (nodeType == XmlNodeType.Element) { close = (_reader.IsEmptyElement) ? "/>" : ">"; // get the line position after the element declaration: // // // is reported with the same position as // // // // The first example has no spaces around '=', the second example does. // while (_reader.MoveToNextAttribute()) { // get line position of the attribute declaration // // ^ // linePosition // lineNumber = _reader.LineNumber; linePosition = _reader.LinePosition + _reader.Name.Length; utilWriter.Write(""; // get line position after the xml declaration: // // // is reported with the same position as // // // // The first example has no spaces around '=', the second example does. // while (_reader.MoveToNextAttribute()) { // get line position of the attribute declaration // // // is reported with the same position as // // // // The first example has one space between 'pi' and "value", the second has multiple spaces. // utilWriter.AppendProcessingInstruction(_reader.Name, _reader.Value); } else if (nodeType == XmlNodeType.EntityReference) { utilWriter.AppendEntityRef(_reader.Name); } else if (nodeType == XmlNodeType.CDATA) { utilWriter.AppendCData(_reader.Value); } else if (nodeType == XmlNodeType.DocumentType) { // // XmlNodeType.DocumentType has the following format: // // // // The reader only gives us the position of 'rootElementName', so we must track what was // written before " // ^ // linePosition lineNumber = _reader.LineNumber; linePosition = _reader.LinePosition + _reader.Name.Length; // Note that there is no way to get the spacing after PUBLIC or SYSTEM attributes and their values if (_reader.MoveToFirstAttribute()) { // Write the space before SYSTEM or PUBLIC utilWriter.AppendRequiredWhiteSpace(lineNumber, linePosition, _reader.LineNumber, _reader.LinePosition); // Write SYSTEM or PUBLIC and the 1st value of the attribute string attrName = _reader.Name; utilWriter.Write(attrName); utilWriter.AppendSpace(); utilWriter.AppendAttributeValue(_reader); _reader.MoveToAttribute(0); // If PUBLIC, write the second value of the attribute if (attrName == "PUBLIC") { _reader.MoveToAttribute(1); utilWriter.AppendSpace(); utilWriter.AppendAttributeValue(_reader); _reader.MoveToAttribute(1); } } // If there is a dtd, write it if (dtdValue != null && dtdValue.Length > 0) { utilWriter.Write(" ["); utilWriter.Write(dtdValue); utilWriter.Write(']'); } utilWriter.Write('>'); } // Advance the _reader so we can get the position of the next node. bool moreToRead = _reader.Read(); nodeType = _reader.NodeType; // Close the node we are copying. if (close != null) { // // Find the position of the close string, for example: // // // ^ // closeLinePosition // int startOffset = GetPositionOffset(nodeType); int closeLineNumber = _reader.LineNumber; int closeLinePosition = _reader.LinePosition - startOffset - close.Length; // Add whitespace up to the position of the close string utilWriter.AppendWhiteSpace(lineNumber, linePosition, closeLineNumber, closeLinePosition); // Write the close string utilWriter.Write(close); } // // Track the position of the reader based on the position of the reader // before we copied this node and what we have written in copying the node. // This allows us to determine the position of the "); return element.ToString(); } // // Copy or replace an element node. // If the element is an empty element, replace it with a formatted start element if either: // * The contents of the start element string need updating. // * The element needs to contain child elements. // // If the element is empty and is replaced with a start/end element pair, return a // end element string with whitespace formatting; otherwise return null. // internal string UpdateStartElement(XmlUtilWriter utilWriter, string updatedStartElement, bool needsChildren, int linePosition, int indent) { Debug.Assert(_reader.NodeType == XmlNodeType.Element, "_reader.NodeType == NodeType.Element"); string endElement = null; bool needsEndElement = false; string elementName; elementName = _reader.Name; // If the element is empty, determine if a new end element is needed. if (_reader.IsEmptyElement) { if (updatedStartElement == null && needsChildren) { updatedStartElement = RetrieveFullOpenElementTag(); } needsEndElement = (updatedStartElement != null); } if (updatedStartElement == null) { // // If no changes to the start element are required, just copy it. // CopyXmlNode(utilWriter); } else { // // Format a new start element/end element pair // string updatedEndElement = ""; string updatedElement = updatedStartElement + updatedEndElement; string formattedElement = FormatXmlElement(updatedElement, linePosition, indent, true); // // Get the start and end element strings from the formatted element. // int iEndElement = formattedElement.LastIndexOf('\n') + 1; string startElement; if (needsEndElement) { endElement = formattedElement.Substring(iEndElement); // Include a newline in the start element as we are expanding an empty element. startElement = formattedElement.Substring(0, iEndElement); } else { // Omit the newline from the start element. startElement = formattedElement.Substring(0, iEndElement - 2); } // Write the new start element. utilWriter.Write(startElement); // Skip over the existing start element. _reader.Read(); } return endElement; } // // Create the cached string writer if it does not exist, // otherwise reuse the existing buffer. // private void ResetCachedStringWriter() { if (_cachedStringWriter == null) { _cachedStringWriter = new StringWriter(new StringBuilder(64), CultureInfo.InvariantCulture); } else { _cachedStringWriter.GetStringBuilder().Length = 0; } } // // Copy a configuration section to a string, and advance the reader. // internal string CopySection() { ResetCachedStringWriter(); // Preserve whitespace for sections for backcompat WhitespaceHandling originalHandling = _reader.WhitespaceHandling; _reader.WhitespaceHandling = WhitespaceHandling.All; // Create string writer to write to XmlUtilWriter utilWriter = new XmlUtilWriter(_cachedStringWriter, false); // Copy the element CopyElement(utilWriter); // Reset whitespace handling _reader.WhitespaceHandling = originalHandling; if ((originalHandling == WhitespaceHandling.None) && (Reader.NodeType == XmlNodeType.Whitespace)) { // If we were previously suppose to skip whitespace, and now we // are at it, then lets jump to the next item _reader.Read(); } utilWriter.Flush(); string s = ((StringWriter)utilWriter.Writer).ToString(); return s; } // Format an Xml element to be written to the config file. // Params: // xmlElement - the element // linePosition - start position of the element // indent - indent for each depth // skipFirstIndent - skip indent for the first element? // static internal string FormatXmlElement(string xmlElement, int linePosition, int indent, bool skipFirstIndent) { XmlParserContext context = new XmlParserContext(null, null, null, XmlSpace.Default, Encoding.Unicode); XmlTextReader reader = new XmlTextReader(xmlElement, XmlNodeType.Element, context); StringWriter stringWriter = new StringWriter(new StringBuilder(64), CultureInfo.InvariantCulture); XmlUtilWriter utilWriter = new XmlUtilWriter(stringWriter, false); // append newline before indent? bool newLine = false; // last node visited was text? bool lastWasText = false; // width of line from end of indentation int lineWidth; // length of the stringbuilder after last indent with newline int sbLengthLastNewLine = 0; while (reader.Read()) { XmlNodeType nodeType = reader.NodeType; if (lastWasText) { utilWriter.Flush(); lineWidth = sbLengthLastNewLine - ((StringWriter)utilWriter.Writer).GetStringBuilder().Length; } else { lineWidth = 0; } switch (nodeType) { case XmlNodeType.CDATA: case XmlNodeType.Element: case XmlNodeType.EndElement: case XmlNodeType.Comment: // Do not indent if the last node was text - doing so would add whitespace // that is included as part of the text. if (!skipFirstIndent && !lastWasText) { utilWriter.AppendIndent(linePosition, indent, reader.Depth, newLine); if (newLine) { utilWriter.Flush(); sbLengthLastNewLine = ((StringWriter)utilWriter.Writer).GetStringBuilder().Length; } } break; default: break; } lastWasText = false; switch (nodeType) { case XmlNodeType.Whitespace: break; case XmlNodeType.SignificantWhitespace: utilWriter.Write(reader.Value); break; case XmlNodeType.CDATA: utilWriter.AppendCData(reader.Value); break; case XmlNodeType.ProcessingInstruction: utilWriter.AppendProcessingInstruction(reader.Name, reader.Value); break; case XmlNodeType.Comment: utilWriter.AppendComment(reader.Value); break; case XmlNodeType.Text: utilWriter.AppendEscapeTextString(reader.Value); lastWasText = true; break; case XmlNodeType.Element: { // Write " MAX_LINE_WIDTH) { utilWriter.AppendIndent(linePosition, indent, reader.Depth - 1, true); lineWidth = indent; writeSpace = false; utilWriter.Flush(); sbLengthLastNewLine = ((StringWriter)utilWriter.Writer).GetStringBuilder().Length; } else { writeSpace = true; } // Write the attribute reader.MoveToNextAttribute(); utilWriter.Flush(); int startLength = ((StringWriter)utilWriter.Writer).GetStringBuilder().Length; if (writeSpace) { utilWriter.AppendSpace(); } utilWriter.Write(reader.Name); utilWriter.Write('='); utilWriter.AppendAttributeValue(reader); utilWriter.Flush(); lineWidth += ((StringWriter)utilWriter.Writer).GetStringBuilder().Length - startLength; } } // position reader back on element reader.MoveToElement(); // write closing tag if (reader.IsEmptyElement) { utilWriter.Write(" />"); } else { utilWriter.Write('>'); } break; case XmlNodeType.EndElement: utilWriter.Write("'); break; case XmlNodeType.EntityReference: utilWriter.AppendEntityRef(reader.Name); break; // Ignore