// Copyright © Microsoft Corporation. // This source file is subject to the Microsoft Permissive License. // See http://www.microsoft.com/resources/sharedsource/licensingbasics/sharedsourcelicenses.mspx. // All other rights reserved. using System; using System.Configuration; using System.Text; using System.Xml; using System.Xml.XPath; using System.IO; namespace Microsoft.Ddue.Tools { public class SaveComponent : BuildComponent { private CustomContext context = new CustomContext(); private XPathExpression path_expression; private XPathExpression select_expression; private XmlWriterSettings settings = new XmlWriterSettings(); public SaveComponent (BuildAssembler assembler, XPathNavigator configuration) : base(assembler, configuration) { // load the target path format XPathNavigator save_node = configuration.SelectSingleNode("save"); if (save_node == null) throw new ConfigurationErrorsException("When instantiating a save component, you must specify a the target file using the element."); string base_value = save_node.GetAttribute("base", String.Empty); if (!String.IsNullOrEmpty(base_value)) { basePath = Path.GetFullPath(Environment.ExpandEnvironmentVariables(base_value)); } string path_value = save_node.GetAttribute("path", String.Empty); if (String.IsNullOrEmpty(path_value)) WriteMessage(MessageLevel.Error, "Each save element must have a path attribute specifying an XPath that evaluates to the location to save the file."); path_expression = XPathExpression.Compile(path_value); string select_value = save_node.GetAttribute("select", String.Empty); if (!String.IsNullOrEmpty(select_value)) select_expression = XPathExpression.Compile(select_value); settings.Encoding = Encoding.UTF8; string indent_value = save_node.GetAttribute("indent", String.Empty); if (!String.IsNullOrEmpty(indent_value)) settings.Indent = Convert.ToBoolean(indent_value); string omit_value = save_node.GetAttribute("omit-xml-declaration", String.Empty); if (!String.IsNullOrEmpty(omit_value)) settings.OmitXmlDeclaration = Convert.ToBoolean(omit_value); linkPath = save_node.GetAttribute("link", String.Empty); if (String.IsNullOrEmpty(linkPath)) linkPath = "../html"; // encoding settings.CloseOutput = true; } private string basePath = null; private string linkPath = null; public override void Apply (XmlDocument document, string key) { // set the evaluation context context["key"] = key; XPathExpression path_xpath = path_expression.Clone(); path_xpath.SetContext(context); // evaluate the path string path = document.CreateNavigator().Evaluate(path_xpath).ToString(); string file = Path.GetFileName(path); string fileLinkPath = Path.Combine(linkPath, file); if (basePath != null) path = Path.Combine(basePath, path); // *SECURIY* The path name may be derived from user entered meta data in Doc Studio and as such // it is not trustworthy. To pass, the path must be inside the directory tree base_directory // (which is the current directory if a path is not specified). // This test is causing problems /* string absoluteBasePath = (basePath == null) ? Directory.GetCurrentDirectory() : Path.GetFullPath(basePath); string targetPath = Path.GetDirectoryName(path); if (!targetPath.StartsWith(absoluteBasePath, StringComparison.CurrentCultureIgnoreCase)) { WriteMessage(MessageLevel.Error, string.Format("Cannot save document outside of base directory: {0}", targetPath)); return; } */ string targetDirectory = Path.GetDirectoryName(path); if (!Directory.Exists(targetDirectory)) Directory.CreateDirectory(targetDirectory); // save the document // select_expression determines which nodes get saved. If there is no select_expression // we simply save the root node as before. If there is a select_expression, we evaluate the // xpath expression and save the resulting node set. The select expression also enables the // "literal-text" processing instruction, which outputs its content as unescaped text. if (select_expression == null) { XmlNode doctype = document.DocumentType; try { //Console.WriteLine("path = '{0}'", path); //document.Save(path); using (XmlWriter writer = XmlWriter.Create(path, settings)) { document.Save(writer); } } catch (IOException e) { WriteMessage(MessageLevel.Error, String.Format("An access error occured while attempting to save to the file '{0}'. The error message is: {1}", path, BuildComponentUtilities.GetExceptionMessage(e))); } catch (XmlException e) { WriteMessage(MessageLevel.Error, String.Format("Invalid XML was written to the output file '{0}'. The error message is: '{1}'", path, BuildComponentUtilities.GetExceptionMessage(e))); } // Get the relative html path for HXF generation. int index = fileLinkPath.IndexOf('/'); string htmlPath = fileLinkPath.Substring(index + 1, fileLinkPath.Length - (index + 1)); FileCreatedEventArgs fe = new FileCreatedEventArgs(htmlPath, Path.GetDirectoryName(targetDirectory)); OnComponentEvent(fe); } else { // IMPLEMENTATION NOTE: The separate StreamWriter is used to maintain XML indenting. // Without it the XmlWriter won't honor our indent settings after plain text nodes have been // written. settings.ConformanceLevel = ConformanceLevel.Auto; using (StreamWriter output = File.CreateText(path)) { using (XmlWriter writer = XmlWriter.Create(output, settings)) { XPathExpression select_xpath = select_expression.Clone(); select_xpath.SetContext(context); XPathNodeIterator ni = document.CreateNavigator().Select(select_expression); while (ni.MoveNext()) { if (ni.Current.NodeType == XPathNodeType.ProcessingInstruction && ni.Current.Name.Equals("literal-text")) { writer.Flush(); output.Write(ni.Current.Value); } else ni.Current.WriteSubtree(writer); } } } } } } }