using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Models.Packaging; using File = System.IO.File; namespace Umbraco.Core.Packaging { /// /// Parses the xml document contained in a compiled (zip) Umbraco package /// internal class CompiledPackageXmlParser { private readonly ConflictingPackageData _conflictingPackageData; public CompiledPackageXmlParser(ConflictingPackageData conflictingPackageData) { _conflictingPackageData = conflictingPackageData; } public CompiledPackage ToCompiledPackage(XDocument xml, string packageFileName, string applicationRootFolder) { if (xml == null) throw new ArgumentNullException(nameof(xml)); if (xml.Root == null) throw new ArgumentException(nameof(xml), "The xml document is invalid"); if (xml.Root.Name != "umbPackage") throw new FormatException("The xml document is invalid"); var info = xml.Root.Element("info"); if (info == null) throw new FormatException("The xml document is invalid"); var package = info.Element("package"); if (package == null) throw new FormatException("The xml document is invalid"); var author = info.Element("author"); if (author == null) throw new FormatException("The xml document is invalid"); var requirements = package.Element("requirements"); if (requirements == null) throw new FormatException("The xml document is invalid"); var def = new CompiledPackage { PackageFileName = packageFileName, Name = package.Element("name")?.Value, Author = author.Element("name")?.Value, AuthorUrl = author.Element("website")?.Value, Version = package.Element("version")?.Value, Readme = info.Element("readme")?.Value, License = package.Element("license")?.Value, LicenseUrl = package.Element("license")?.AttributeValue("url"), Url = package.Element("url")?.Value, IconUrl = package.Element("iconUrl")?.Value, UmbracoVersion = new Version((int)requirements.Element("major"), (int)requirements.Element("minor"), (int)requirements.Element("patch")), UmbracoVersionRequirementsType = requirements.AttributeValue("type").IsNullOrWhiteSpace() ? RequirementsType.Legacy : Enum.Parse(requirements.AttributeValue("type")), Control = package.Element("control")?.Value, Actions = xml.Root.Element("Actions")?.ToString(SaveOptions.None) ?? "", //take the entire outer xml value Files = xml.Root.Element("files")?.Elements("file")?.Select(x => new CompiledPackageFile { UniqueFileName = x.Element("guid")?.Value, OriginalName = x.Element("orgName")?.Value, OriginalPath = x.Element("orgPath")?.Value }).ToList() ?? new List(), Macros = xml.Root.Element("Macros")?.Elements("macro") ?? Enumerable.Empty(), Templates = xml.Root.Element("Templates")?.Elements("Template") ?? Enumerable.Empty(), Stylesheets = xml.Root.Element("Stylesheets")?.Elements("styleSheet") ?? Enumerable.Empty(), DataTypes = xml.Root.Element("DataTypes")?.Elements("DataType") ?? Enumerable.Empty(), Languages = xml.Root.Element("Languages")?.Elements("Language") ?? Enumerable.Empty(), DictionaryItems = xml.Root.Element("DictionaryItems")?.Elements("DictionaryItem") ?? Enumerable.Empty(), DocumentTypes = xml.Root.Element("DocumentTypes")?.Elements("DocumentType") ?? Enumerable.Empty(), Documents = xml.Root.Element("Documents")?.Elements("DocumentSet") ?? Enumerable.Empty(), }; def.Warnings = GetPreInstallWarnings(def, applicationRootFolder); return def; } private PreInstallWarnings GetPreInstallWarnings(CompiledPackage package, string applicationRootFolder) { var sourceDestination = ExtractSourceDestinationFileInformation(package.Files); var installWarnings = new PreInstallWarnings { ConflictingMacros = _conflictingPackageData.FindConflictingMacros(package.Macros), ConflictingTemplates = _conflictingPackageData.FindConflictingTemplates(package.Templates), ConflictingStylesheets = _conflictingPackageData.FindConflictingStylesheets(package.Stylesheets), UnsecureFiles = FindUnsecureFiles(sourceDestination), FilesReplaced = FindFilesToBeReplaced(sourceDestination, applicationRootFolder) }; return installWarnings; } /// /// Returns a tuple of the zip file's unique file name and it's application relative path /// /// /// public (string packageUniqueFile, string appRelativePath)[] ExtractSourceDestinationFileInformation(IEnumerable packageFiles) { return packageFiles .Select(e => { var fileName = PrepareAsFilePathElement(e.OriginalName); var relativeDir = UpdatePathPlaceholders(PrepareAsFilePathElement(e.OriginalPath)); var relativePath = Path.Combine(relativeDir, fileName); return (e.UniqueFileName, relativePath); }).ToArray(); } private IEnumerable FindFilesToBeReplaced(IEnumerable<(string packageUniqueFile, string appRelativePath)> sourceDestination, string applicationRootFolder) { return sourceDestination.Where(sd => File.Exists(Path.Combine(applicationRootFolder, sd.appRelativePath))) .Select(x => x.appRelativePath) .ToArray(); } private IEnumerable FindUnsecureFiles(IEnumerable<(string packageUniqueFile, string appRelativePath)> sourceDestinationPair) { return sourceDestinationPair.Where(sd => IsFileDestinationUnsecure(sd.appRelativePath)) .Select(x => x.appRelativePath) .ToList(); } private bool IsFileDestinationUnsecure(string destination) { var unsecureDirNames = new[] { "bin", "app_code" }; if (unsecureDirNames.Any(ud => destination.StartsWith(ud, StringComparison.InvariantCultureIgnoreCase))) return true; string extension = Path.GetExtension(destination); return extension != null && extension.Equals(".dll", StringComparison.InvariantCultureIgnoreCase); } private static string PrepareAsFilePathElement(string pathElement) { return pathElement.TrimStart(new[] { '\\', '/', '~' }).Replace("/", "\\"); } private static string UpdatePathPlaceholders(string path) { if (path.Contains("[$")) { //this is experimental and undocumented... path = path.Replace("[$UMBRACO]", SystemDirectories.Umbraco); path = path.Replace("[$CONFIG]", SystemDirectories.Config); path = path.Replace("[$DATA]", SystemDirectories.Data); } return path; } public IEnumerable GetPackageActions(XElement actionsElement, string packageName) { if (actionsElement == null) { return new PackageAction[0]; } if (string.Equals("Actions", actionsElement.Name.LocalName) == false) throw new ArgumentException($"Must be \"Actions\" as root", nameof(actionsElement)); return actionsElement.Elements("Action") .Select(elemet => { var aliasAttr = elemet.Attribute("Alias"); if (aliasAttr == null) throw new ArgumentException("missing \"Alias\" atribute in alias element", nameof(actionsElement)); var packageAction = new PackageAction { XmlData = elemet, Alias = aliasAttr.Value, PackageName = packageName, }; var attr = elemet.Attribute("runat"); if (attr != null && Enum.TryParse(attr.Value, true, out ActionRunAt runAt)) { packageAction.RunAt = runAt; } attr = elemet.Attribute("undo"); if (attr != null && bool.TryParse(attr.Value, out var undo)) { packageAction.Undo = undo; } return packageAction; }).ToArray(); } } }