File: XslTransformation.cs
Web Access
Project: ..\..\..\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.Reflection;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Utilities;
 
#nullable disable
 
namespace Microsoft.Build.Tasks
{
    /// <summary>
    /// A task that transforms a XML input with an XSLT or Compiled XSLT
    /// and outputs to screen or specified file.
    /// </summary>
    public class XslTransformation : TaskExtension
    {
        #region Members
 
        /// <summary>
        /// The output files.
        /// </summary>
        private ITaskItem[] _outputPaths;
 
        #endregion
 
        #region Properties
        /// <summary>
        /// The XML input as file path.
        /// </summary>
        public ITaskItem[] XmlInputPaths { get; set; }
 
        /// <summary>
        /// The XML input as string.
        /// </summary>
        public string XmlContent { get; set; }
 
        /// <summary>
        /// The XSLT input as file path.
        /// </summary>
        public ITaskItem XslInputPath { get; set; }
 
        /// <summary>
        /// The XSLT input as string.
        /// </summary>
        public string XslContent { get; set; }
 
        /// <summary>
        /// Flag to preserve whitespaces in the XSLT file.
        /// </summary>
        public bool PreserveWhitespace { get; set; }
 
        /// <summary>
        /// The XSLT input as compiled dll.
        /// </summary>
        public ITaskItem XslCompiledDllPath { get; set; }
 
        /// <summary>
        /// The output file.
        /// </summary>
        [Required]
        public ITaskItem[] OutputPaths
        {
            get
            {
                ErrorUtilities.VerifyThrowArgumentNull(_outputPaths, "OutputPath");
                return _outputPaths;
            }
 
            set => _outputPaths = value;
        }
 
        /// <summary>
        /// The parameters to XSLT Input document.
        /// </summary>
        public string Parameters { get; set; }
 
        /// <summary>
        /// Determines whether or not to use trusted settings. Default is false.
        /// </summary>
        public bool UseTrustedSettings { get; set; }
 
        #endregion
 
        /// <summary>
        /// Executes the XslTransform task.
        /// </summary>
        /// <returns>true if transformation succeeds.</returns>
        public override bool Execute()
        {
            XmlInput xmlinput;
            XsltInput xsltinput;
            ErrorUtilities.VerifyThrowArgumentNull(_outputPaths, "OutputPath");
 
            // Load XmlInput, XsltInput parameters
            try
            {
                xmlinput = new XmlInput(XmlInputPaths, XmlContent);
                xsltinput = new XsltInput(XslInputPath, XslContent, XslCompiledDllPath, Log, PreserveWhitespace);
            }
            catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
            {
                Log.LogErrorWithCodeFromResources("XslTransform.ArgumentError", e.Message);
                return false;
            }
 
            // Check if OutputPath has same number of parameters as xmlInputPaths.
            if (XmlInputPaths != null && XmlInputPaths.Length != _outputPaths.Length)
            {
                Log.LogErrorWithCodeFromResources("General.TwoVectorsMustHaveSameLength", _outputPaths.Length, XmlInputPaths.Length, "OutputPaths", "XmlInputPaths");
                return false;
            }
 
            // Check if OutputPath has 1 parameter if xmlString is specified.
            if (XmlContent != null && _outputPaths.Length != 1)
            {
                Log.LogErrorWithCodeFromResources("General.TwoVectorsMustHaveSameLength", _outputPaths.Length, 1, "OutputPaths", "XmlContent");
                return false;
            }
 
            XsltArgumentList arguments;
 
            // Arguments parameters
            try
            {
                arguments = ProcessXsltArguments(Parameters);
            }
            catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
            {
                Log.LogErrorWithCodeFromResources("XslTransform.XsltArgumentsError", e.Message);
                return false;
            }
 
            XslCompiledTransform xslct;
 
            // Load the XSLT
            try
            {
                xslct = xsltinput.LoadXslt(UseTrustedSettings);
            }
            catch (PlatformNotSupportedException)
            {
                Log.LogErrorWithCodeFromResources("XslTransform.PrecompiledXsltError");
                return false;
            }
            catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
            {
                Log.LogErrorWithCodeFromResources("XslTransform.XsltLoadError", e.Message);
                return false;
            }
 
            // Do the transformation.
            try
            {
                if (UseTrustedSettings)
                {
                    Log.LogMessageFromResources(MessageImportance.High, "XslTransform.SecuritySettingsViaUseTrustedSettings");
                }
 
                for (int i = 0; i < xmlinput.Count; i++)
                {
                    using (XmlWriter xmlWriter = XmlWriter.Create(_outputPaths[i].ItemSpec, xslct.OutputSettings))
                    {
                        using (XmlReader xr = xmlinput.CreateReader(i))
                        {
                            xslct.Transform(xr, arguments, xmlWriter, new XmlUrlResolver());
                        }
 
                        xmlWriter.Close();
                    }
                }
            }
            catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
            {
                string flattenedMessage = TaskLoggingHelper.GetInnerExceptionMessageString(e);
                Log.LogErrorWithCodeFromResources("XslTransform.TransformError", flattenedMessage);
                Log.LogMessage(MessageImportance.Low, e.ToString());
                return false;
            }
 
            // Copy Metadata
            if (xmlinput.XmlMode == XmlInput.XmlModes.XmlFile)
            {
                for (int i = 0; i < XmlInputPaths.Length; i++)
                {
                    XmlInputPaths[i].CopyMetadataTo(_outputPaths[i]);
                }
            }
 
            return true;
        }
 
        /// <summary>
        /// Takes the raw XML and loads XsltArgumentList
        /// </summary>
        /// <param name="xsltParametersXml">The raw XML that holds each parameter as <Parameter Name="" Value="" Namespace="" /> </param>
        /// <returns>XsltArgumentList.</returns>
        private static XsltArgumentList ProcessXsltArguments(string xsltParametersXml)
        {
            XsltArgumentList arguments = new XsltArgumentList();
            if (xsltParametersXml == null)
            {
                return arguments;
            }
 
            XmlDocument doc = new XmlDocument();
            try
            {
                XmlReaderSettings settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore };
                using (XmlReader reader = XmlReader.Create(new StringReader("<XsltParameters>" + xsltParametersXml + "</XsltParameters>"), settings))
                {
                    doc.Load(reader);
                }
            }
            catch (XmlException xe)
            {
                throw new ArgumentException(ResourceUtilities.GetResourceString("XslTransform.XsltParameterNotWellFormed"), xe);
            }
 
            XmlNodeList xnl = doc.SelectNodes("/XsltParameters/*[local-name() = 'Parameter']");
 
            for (int i = 0; i < xnl.Count; i++)
            {
                XmlNode xn = xnl[i];
 
                if (xn.Attributes["Name"] == null)
                {
                    throw new ArgumentException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("XslTransform.XsltParameterNoAttribute", "Name"));
                }
 
                if (xn.Attributes["Value"] == null)
                {
                    throw new ArgumentException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("XslTransform.XsltParameterNoAttribute", "Value"));
                }
 
                string ns = String.Empty;
                if (xn.Attributes["Namespace"] != null)
                {
                    ns = xn.Attributes["Namespace"].Value;
                }
 
                arguments.AddParam(xn.Attributes["Name"].Value, ns, xn.Attributes["Value"].Value);
            }
 
            return arguments;
        }
 
        #region Supporting classes for input
        /// <summary>
        /// This class prepares XML input from XmlFile and Xml parameters
        /// </summary>
        internal class XmlInput
        {
            /// <summary>
            /// This either contains the raw Xml or the path to Xml file.
            /// </summary>
            private readonly string[] _data;
 
            /// <summary>
            /// Constructor.
            /// Only one parameter should be non null or will throw ArgumentException.
            /// </summary>
            /// <param name="xmlFile">The path to XML file or null.</param>
            /// <param name="xml">The raw XML.</param>
            public XmlInput(ITaskItem[] xmlFile, string xml)
            {
                if (xmlFile != null && xml != null)
                {
                    throw new ArgumentException(ResourceUtilities.GetResourceString("XslTransform.XmlInput.TooMany"));
                }
                else if (xmlFile == null && xml == null)
                {
                    throw new ArgumentException(ResourceUtilities.GetResourceString("XslTransform.XmlInput.TooFew"));
                }
 
                if (xmlFile != null)
                {
                    XmlMode = XmlModes.XmlFile;
                    _data = new string[xmlFile.Length];
                    for (int i = 0; i < xmlFile.Length; i++)
                    {
                        _data[i] = xmlFile[i].ItemSpec;
                    }
                }
                else
                {
                    XmlMode = XmlModes.Xml;
                    _data = new[] { xml };
                }
            }
 
            /// <summary>
            /// Possible accepted types of XML input.
            /// </summary>
            public 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 count of Xml Inputs
            /// </summary>
            public int Count => _data.Length;
 
            /// <summary>
            /// Returns the current mode of the XmlInput
            /// </summary>
            public XmlModes XmlMode { get; }
 
            /// <summary>
            /// Creates correct reader based on the input type.
            /// </summary>
            /// <returns>The XmlReader object</returns>
            public XmlReader CreateReader(int itemPos)
            {
                if (XmlMode == XmlModes.XmlFile)
                {
                    return XmlReader.Create(new StreamReader(_data[itemPos]), new XmlReaderSettings { CloseInput = true }, _data[itemPos]);
                }
                else // xmlModes.Xml
                {
                    return XmlReader.Create(new StringReader(_data[itemPos]));
                }
            }
        }
 
        /// <summary>
        /// This class prepares XSLT input from XsltFile, Xslt and XsltCompiledDll parameters
        /// </summary>
        internal class XsltInput
        {
            /// <summary>
            /// What XSLT input type are we at.
            /// </summary>
            private readonly XslModes _xslMode;
 
            /// <summary>
            /// Contains the raw XSLT
            /// or the path to XSLT file
            /// or the path to compiled XSLT dll.
            /// </summary>
            private readonly string _data;
 
            /// <summary>
            /// Flag to preserve whitespaces in the XSLT file.
            /// </summary>
            private bool _preserveWhitespace;
 
            /// <summary>
            /// Tool for logging build messages, warnings, and errors
            /// </summary>
            private readonly TaskLoggingHelper _log;
 
            /// <summary>
            /// Constructer.
            /// Only one parameter should be non null or will throw ArgumentException.
            /// </summary>
            /// <param name="xsltFile">The path to XSLT file or null.</param>
            /// <param name="xslt">The raw to XSLT or null.</param>
            /// <param name="xsltCompiledDll">The path to compiled XSLT file or null.</param>
            /// <param name="logTool">Log helper.</param>
            /// <param name="preserveWhitespace">Flag for xslt whitespace option.</param>
            public XsltInput(ITaskItem xsltFile, string xslt, ITaskItem xsltCompiledDll, TaskLoggingHelper logTool, bool preserveWhitespace)
            {
                _log = logTool;
                if ((xsltFile != null && xslt != null) ||
                    (xsltFile != null && xsltCompiledDll != null) ||
                    (xslt != null && xsltCompiledDll != null))
                {
                    throw new ArgumentException(ResourceUtilities.GetResourceString("XslTransform.XsltInput.TooMany"));
                }
                else if (xsltFile == null && xslt == null && xsltCompiledDll == null)
                {
                    throw new ArgumentException(ResourceUtilities.GetResourceString("XslTransform.XsltInput.TooFew"));
                }
 
                if (xsltFile != null)
                {
                    _xslMode = XslModes.XsltFile;
                    _data = xsltFile.ItemSpec;
                }
                else if (xslt != null)
                {
                    _xslMode = XslModes.Xslt;
                    _data = xslt;
                }
                else
                {
                    _xslMode = XslModes.XsltCompiledDll;
                    _data = xsltCompiledDll.ItemSpec;
                }
 
                _preserveWhitespace = preserveWhitespace;
            }
 
            /// <summary>
            /// Possible accepted types of XSLT input.
            /// </summary>
            public enum XslModes
            {
                /// <summary>
                /// If the mode is a XSLT file.
                /// </summary>
                XsltFile,
 
                /// <summary>
                /// If the mode is a raw XSLT.
                /// </summary>
                Xslt,
 
                /// <summary>
                /// If the mode is a compiled Xslt dll.
                /// </summary>
                XsltCompiledDll
            }
 
            /// <summary>
            /// Loads the XSLT to XslCompiledTransform. By default uses Default settings instead of trusted settings.
            /// </summary>
            /// <returns>A XslCompiledTransform object.</returns>
            public XslCompiledTransform LoadXslt()
            {
                return LoadXslt(false);
            }
 
            /// <summary>
            /// Loads the XSLT to XslCompiledTransform. By default uses Default settings instead of trusted settings.
            /// </summary>
            /// <param name="useTrustedSettings">Determines whether or not to use trusted settings.</param>
            /// <returns>A XslCompiledTransform object.</returns>
            public XslCompiledTransform LoadXslt(bool useTrustedSettings)
            {
                XslCompiledTransform xslct = new XslCompiledTransform();
                XsltSettings settings = XsltSettings.Default;
 
                switch (_xslMode)
                {
                    case XslModes.Xslt:
                        {
                            using var sr = new StringReader(_data);
                            using var xmlReader = XmlReader.Create(sr);
                            xslct.Load(xmlReader, settings, new XmlUrlResolver());
                            break;
                        }         
                    case XslModes.XsltFile:
                        if (useTrustedSettings)
                        {
                            settings = XsltSettings.TrustedXslt;
                        }
                        else
                        {
                            _log.LogMessageFromResources(MessageImportance.Low, "XslTransform.UseTrustedSettings", _data);
                        }
 
                        using (XmlReader reader = XmlReader.Create(new StreamReader(_data), new XmlReaderSettings { CloseInput = true }, _data))
                        {
                            XmlSpace xmlSpaceOption = _preserveWhitespace ? XmlSpace.Preserve : XmlSpace.Default;
                            xslct.Load(new XPathDocument(reader, xmlSpaceOption), settings, new XmlUrlResolver());
                        }
                        break;
                    case XslModes.XsltCompiledDll:
#if FEATURE_COMPILED_XSL
                        // We accept type in format: assembly_name[;type_name]. type_name may be omitted if assembly has just one type defined
                        string dll = _data;
                        string[] pair = dll.Split(MSBuildConstants.SemicolonChar);
                        string assemblyPath = pair[0];
                        string typeName = (pair.Length == 2) ? pair[1] : null;
 
                        Type t = FindType(assemblyPath, typeName);
                        xslct.Load(t);
                        break;
#else
                        throw new PlatformNotSupportedException("Precompiled XSLTs are not supported in .NET Core");
#endif
                    default:
                        ErrorUtilities.ThrowInternalErrorUnreachable();
                        break;
                }
 
                return xslct;
            }
 
            /// <summary>
            /// Find the type from an assembly and loads it.
            /// </summary>
            /// <param name="assemblyPath">The path to assembly.</param>
            /// <param name="typeName">The type name.</param>
            /// <returns>Found type.</returns>
            private static Type FindType(string assemblyPath, string typeName)
            {
                AssemblyName assemblyName = new AssemblyName { CodeBase = assemblyPath };
                Assembly loadedAssembly = Assembly.Load(assemblyName);
                if (typeName != null)
                {
                    return loadedAssembly.GetType(typeName);
                }
                else
                {
                    var types = new List<Type>();
                    foreach (Type type in loadedAssembly.GetTypes())
                    {
                        if (!type.Name.StartsWith("$", StringComparison.Ordinal))
                        {
                            types.Add(type);
                        }
                    }
 
                    if (types.Count == 1)
                    {
                        return types[0];
                    }
 
                    throw new ArgumentException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("XslTransform.MustSpecifyType", assemblyPath));
                }
            }
        }
        #endregion
    }
}