| File: XmlPeek.cs | Web Access |
| Project: src\msbuild\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.IO; using System.Xml; using System.Xml.XPath; using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Utilities; #nullable disable namespace Microsoft.Build.Tasks { /// <summary> /// A task that returns values as specified by XPath Query /// from an XML file. /// </summary> [MSBuildMultiThreadableTask] public class XmlPeek : TaskExtension, IMultiThreadableTask { #region Properties /// <inheritdoc /> public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; /// <summary> /// The XPath Query. /// </summary> public ITaskItem XmlInputPath { get; set; } /// <summary> /// The XML input as a string. /// </summary> public string XmlContent { get; set; } /// <summary> /// The XPath Query. /// </summary> [Required] public string Query { get; set; } /// <summary> /// The results returned by this task. /// </summary> [Output] public ITaskItem[] Result { get; private set; } /// <summary> /// The namespaces for XPath query's prefixes. /// </summary> public string Namespaces { get; set; } /// <summary> /// Prohibits loading XML with embedded DTD. When true (default), an error is raised if a DTD is present. /// Set to false to opt out of DTD rejection and continue processing while ignoring the DTD. /// </summary> public bool ProhibitDtd { get; set; } = ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_11); #endregion /// <summary> /// Executes the XMLPeek task. /// </summary> /// <returns>true if transformation succeeds.</returns> public override bool Execute() { XmlInput xmlinput; try { AbsolutePath? absoluteInputPath = XmlInputPath != null ? TaskEnvironment.GetAbsolutePath(XmlInputPath.ItemSpec) : null; xmlinput = new XmlInput(absoluteInputPath, XmlContent); } catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) { Log.LogErrorWithCodeFromResources("XmlPeek.ArgumentError", e.Message); return false; } XPathDocument xpathdoc; try { // Load the XPath Document using (XmlReader xr = xmlinput.CreateReader(ProhibitDtd)) { xpathdoc = new XPathDocument(xr); } } catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) { if (XmlTaskUtility.IsDtdProhibitedException(e, ProhibitDtd, xmlinput.ContainsDtd)) { Log.LogErrorWithCodeFromResources("XmlPeekPoke.InputDtdProhibited", nameof(ProhibitDtd), nameof(XmlPeek)); } else { Log.LogErrorWithCodeFromResources("XmlPeekPoke.InputFileError", XmlInputPath?.ItemSpec, e.Message); } return false; } finally { xmlinput.CloseReader(); } XPathNavigator nav = xpathdoc.CreateNavigator(); XPathExpression expr; try { // Create the expression from query expr = nav.Compile(Query); } catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) { Log.LogErrorWithCodeFromResources("XmlPeekPoke.XPathError", Query, e.Message); return false; } // Create the namespace manager and parse the input. var xmlNamespaceManager = new XmlNamespaceManager(nav.NameTable); try { LoadNamespaces(ref xmlNamespaceManager, Namespaces); } catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) { Log.LogErrorWithCodeFromResources("XmlPeek.NamespacesError", e.Message); return false; } try { expr.SetContext(xmlNamespaceManager); } catch (XPathException e) { Log.LogErrorWithCodeFromResources("XmlPeek.XPathContextError", e.Message); return false; } XPathNodeIterator iter = nav.Select(expr); var peekValues = new List<string>(); while (iter.MoveNext()) { if (iter.Current.NodeType == XPathNodeType.Attribute || iter.Current.NodeType == XPathNodeType.Text) { peekValues.Add(iter.Current.Value); } else { peekValues.Add(iter.Current.OuterXml); } } Result = new ITaskItem[peekValues.Count]; int i = 0; foreach (string item in peekValues) { Result[i++] = new TaskItem(EscapingUtilities.Escape(item)); // This can be logged a lot, so low importance Log.LogMessageFromResources(MessageImportance.Low, "XmlPeek.Found", item); } if (Result.Length == 0) { // Logged no more than once per execute of this task Log.LogMessageFromResources("XmlPeek.NotFound"); } return true; } /// <summary> /// Loads the namespaces specified at Namespaces parameter to XmlNSManager. /// </summary> /// <param name="namespaceManager">The namespace manager to load namespaces to.</param> /// <param name="namepaces">The namespaces as XML snippet.</param> private static void LoadNamespaces(ref XmlNamespaceManager namespaceManager, string namepaces) { var doc = new XmlDocument(); try { var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }; using (XmlReader reader = XmlReader.Create(new StringReader("<Namespaces>" + namepaces + "</Namespaces>"), settings)) { doc.Load(reader); } } catch (XmlException xe) { throw new ArgumentException(ResourceUtilities.GetResourceString("XmlPeek.NamespacesParameterNotWellFormed"), xe); } XmlNodeList xnl = doc.SelectNodes("/Namespaces/*[local-name() = 'Namespace']"); for (int i = 0; i < xnl?.Count; i++) { XmlNode xn = xnl[i]; const string prefixAttr = "Prefix"; XmlAttribute prefix = xn.Attributes?[prefixAttr]; if (prefix == null) { throw new ArgumentException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("XmlPeek.NamespacesParameterNoAttribute", prefixAttr)); } const string uriAttr = "Uri"; XmlAttribute uri = xn.Attributes[uriAttr]; if (uri == null) { throw new ArgumentException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("XmlPeek.NamespacesParameterNoAttribute", uriAttr)); } namespaceManager.AddNamespace(prefix.Value, uri.Value); } } /// <summary> /// This class prepares XML input from XMLInputPath and XMLContent parameters /// </summary> private sealed class XmlInput { /// <summary> /// Contains the absolute path to Xml file when in XmlFile mode. /// </summary> private readonly AbsolutePath? _filePath; /// <summary> /// Contains the raw Xml content when in Xml mode. /// </summary> private readonly string _xmlContent; /// <summary> /// FileStream used to read XML. /// </summary> private FileStream _fs; /// <summary> /// Constructor. /// Only one parameter should be non null or will throw ArgumentException. /// </summary> /// <param name="xmlInputPath">The absolute path to XML file or null.</param> /// <param name="xmlContent">The raw XML.</param> public XmlInput(AbsolutePath? xmlInputPath, string xmlContent) { if (xmlInputPath != null && xmlContent != null) { throw new ArgumentException(ResourceUtilities.GetResourceString("XmlPeek.XmlInput.TooMany")); } if (xmlInputPath == null && xmlContent == null) { throw new ArgumentException(ResourceUtilities.GetResourceString("XmlPeek.XmlInput.TooFew")); } if (xmlInputPath != null) { XmlMode = XmlModes.XmlFile; _filePath = xmlInputPath; } else { XmlMode = XmlModes.Xml; _xmlContent = xmlContent; } } /// <summary> /// Possible accepted types of XML input. /// </summary> private enum XmlModes { /// <summary> /// If the mode is a XML file. /// </summary> XmlFile, /// <summary> /// If the mode is a raw XML. /// </summary> Xml } /// <summary> /// Returns the current mode of the XmlInput /// </summary> private XmlModes XmlMode { get; } /// <summary> /// Creates correct reader based on the input type. /// </summary> /// <returns>The XmlReader object</returns> public XmlReader CreateReader(bool prohibitDtd) { var settings = new XmlReaderSettings { DtdProcessing = prohibitDtd ? DtdProcessing.Prohibit : DtdProcessing.Ignore }; if (XmlMode == XmlModes.XmlFile) { _fs = new FileStream(_filePath.Value, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); return XmlReader.Create(_fs, settings: settings); } else // xmlModes.Xml { return XmlReader.Create(new StringReader(_xmlContent), settings: settings); } } public bool ContainsDtd() { if (XmlMode == XmlModes.XmlFile) { return XmlTaskUtility.ContainsDtd(_filePath.Value); } return XmlTaskUtility.ContainsDtd(_xmlContent); } /// <summary> /// Closes the reader. /// </summary> public void CloseReader() { if (_fs != null) { _fs.Dispose(); _fs = null; } } } } }