//------------------------------------------------------------------------------
//
// 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("");
utilWriter.Write(_reader.Name);
}
else if (nodeType == XmlNodeType.Comment) {
utilWriter.AppendComment(_reader.Value);
}
else if (nodeType == XmlNodeType.Text) {
utilWriter.AppendEscapeTextString(_reader.Value);
}
else if (nodeType == XmlNodeType.XmlDeclaration) {
close = "?>";
// 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 = "" + elementName + ">";
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("");
utilWriter.Write(reader.Name);
utilWriter.Write('>');
break;
case XmlNodeType.EntityReference:
utilWriter.AppendEntityRef(reader.Name);
break;
// Ignore