Files
Umbraco-CMS/src/Umbraco.Core/Packaging/CompiledPackageXmlParser.cs
2019-01-14 17:56:41 +11:00

188 lines
9.0 KiB
C#

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
{
/// <summary>
/// Parses the xml document contained in a compiled (zip) Umbraco package
/// </summary>
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<string>("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<string>("type").IsNullOrWhiteSpace() ? RequirementsType.Legacy : Enum<RequirementsType>.Parse(requirements.AttributeValue<string>("type")),
Control = package.Element("control")?.Value,
Actions = xml.Root.Element("Actions")?.ToString(SaveOptions.None) ?? "<Actions></Actions>", //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<CompiledPackageFile>(),
Macros = xml.Root.Element("Macros")?.Elements("macro") ?? Enumerable.Empty<XElement>(),
Templates = xml.Root.Element("Templates")?.Elements("Template") ?? Enumerable.Empty<XElement>(),
Stylesheets = xml.Root.Element("Stylesheets")?.Elements("styleSheet") ?? Enumerable.Empty<XElement>(),
DataTypes = xml.Root.Element("DataTypes")?.Elements("DataType") ?? Enumerable.Empty<XElement>(),
Languages = xml.Root.Element("Languages")?.Elements("Language") ?? Enumerable.Empty<XElement>(),
DictionaryItems = xml.Root.Element("DictionaryItems")?.Elements("DictionaryItem") ?? Enumerable.Empty<XElement>(),
DocumentTypes = xml.Root.Element("DocumentTypes")?.Elements("DocumentType") ?? Enumerable.Empty<XElement>(),
Documents = xml.Root.Element("Documents")?.Elements("DocumentSet") ?? Enumerable.Empty<XElement>(),
};
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;
}
/// <summary>
/// Returns a tuple of the zip file's unique file name and it's application relative path
/// </summary>
/// <param name="packageFiles"></param>
/// <returns></returns>
public (string packageUniqueFile, string appRelativePath)[] ExtractSourceDestinationFileInformation(IEnumerable<CompiledPackageFile> 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<string> 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<string> 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<PackageAction> 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();
}
}
}