using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Linq; using System.Xml.Linq; using System.Xml.XPath; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Models.Packaging; using Umbraco.Core.Services; using File = System.IO.File; namespace Umbraco.Core.Packaging { internal class PackageInstallation : IPackageInstallation { private readonly IFileService _fileService; private readonly IMacroService _macroService; private readonly IPackagingService _packagingService; private IConflictingPackageData _conflictingPackageData; private readonly IPackageExtraction _packageExtraction; private string _fullPathToRoot; public PackageInstallation(IPackagingService packagingService, IMacroService macroService, IFileService fileService, IPackageExtraction packageExtraction) : this(packagingService, macroService, fileService, packageExtraction, IOHelper.GetRootDirectorySafe()) {} public PackageInstallation(IPackagingService packagingService, IMacroService macroService, IFileService fileService, IPackageExtraction packageExtraction, string fullPathToRoot) { if (packageExtraction != null) _packageExtraction = packageExtraction; else throw new ArgumentNullException("packageExtraction"); if (macroService != null) _macroService = macroService; else throw new ArgumentNullException("macroService"); if (fileService != null) _fileService = fileService; else throw new ArgumentNullException("fileService"); if (packagingService != null) _packagingService = packagingService; else throw new ArgumentNullException("packagingService"); _fullPathToRoot = fullPathToRoot; } public IConflictingPackageData ConflictingPackageData { private get { return _conflictingPackageData ?? (_conflictingPackageData = new ConflictingPackageData(_macroService, _fileService)); } set { if (_conflictingPackageData != null) { throw new PropertyConstraintException("This property already have a value"); } _conflictingPackageData = value; } } public string FullPathToRoot { private get { return _fullPathToRoot; } set { if (_fullPathToRoot != null) { throw new PropertyConstraintException("This property already have a value"); } _fullPathToRoot = value; } } public MetaData GetMetaData(string packageFilePath) { try { XElement rootElement = GetConfigXmlElement(packageFilePath); return GetMetaData(rootElement); } catch (Exception e) { throw new Exception("Error reading " + packageFilePath, e); } } public PreInstallWarnings GetPreInstallWarnings(string packageFilePath) { try { XElement rootElement = GetConfigXmlElement(packageFilePath); return GetPreInstallWarnings(rootElement); } catch (Exception e) { throw new Exception("Error reading " + packageFilePath, e); } } public InstallationSummary InstallPackage(string packageFile, int userId) { XElement dataTypes; XElement languages; XElement dictionaryItems; XElement macroes; XElement files; XElement templates; XElement documentTypes; XElement styleSheets; XElement documentSet; XElement documents; XElement actions; MetaData metaData; InstallationSummary installationSummary; try { XElement rootElement = GetConfigXmlElement(packageFile); PackageSupportedCheck(rootElement); PackageStructureSanetyCheck(packageFile, rootElement); dataTypes = rootElement.Element(Constants.Packaging.DataTypesNodeName); languages = rootElement.Element(Constants.Packaging.LanguagesNodeName); dictionaryItems = rootElement.Element(Constants.Packaging.DictionaryItemsNodeName); macroes = rootElement.Element(Constants.Packaging.MacrosNodeName); files = rootElement.Element(Constants.Packaging.FilesNodeName); templates = rootElement.Element(Constants.Packaging.TemplatesNodeName); documentTypes = rootElement.Element(Constants.Packaging.DocumentTypesNodeName); styleSheets = rootElement.Element(Constants.Packaging.StylesheetsNodeName); documentSet = rootElement.Element(Constants.Packaging.DocumentSetNodeName); documents = rootElement.Element(Constants.Packaging.DocumentsNodeName); actions = rootElement.Element(Constants.Packaging.ActionsNodeName); metaData = GetMetaData(rootElement); installationSummary = new InstallationSummary {MetaData = metaData}; } catch (Exception e) { throw new Exception("Error reading " + packageFile, e); } try { var dataTypeDefinitions = EmptyEnumerableIfNull(dataTypes) ?? InstallDataTypes(dataTypes, userId); installationSummary.DataTypesInstalled = dataTypeDefinitions; var languagesInstalled = EmptyEnumerableIfNull(languages) ?? InstallLanguages(languages, userId); installationSummary.LanguagesInstalled = languagesInstalled; var dictionaryInstalled = EmptyEnumerableIfNull(dictionaryItems) ?? InstallDictionaryItems(dictionaryItems); installationSummary.DictionaryItemsInstalled = dictionaryInstalled; var macros = EmptyEnumerableIfNull(macroes) ?? InstallMacros(macroes, userId); installationSummary.MacrosInstalled = macros; var keyValuePairs = EmptyEnumerableIfNull(packageFile) ?? InstallFiles(packageFile, files); installationSummary.FilesInstalled = keyValuePairs; var templatesInstalled = EmptyEnumerableIfNull(templates) ?? InstallTemplats(templates, userId); installationSummary.TemplatesInstalled = templatesInstalled; var documentTypesInstalled = EmptyEnumerableIfNull(documentTypes) ?? InstallDocumentTypes(documentTypes, userId); installationSummary.ContentTypesInstalled =documentTypesInstalled; var stylesheetsInstalled = EmptyEnumerableIfNull(styleSheets) ?? InstallStylesheets(styleSheets); installationSummary.StylesheetsInstalled = stylesheetsInstalled; var documentsInstalled = documents != null ? InstallDocuments(documents, userId) : EmptyEnumerableIfNull(documentSet) ?? InstallDocuments(documentSet, userId); installationSummary.ContentInstalled = documentsInstalled; var packageActions = EmptyEnumerableIfNull(actions) ?? GetPackageActions(actions, metaData.Name); installationSummary.Actions = packageActions; installationSummary.PackageInstalled = true; return installationSummary; } catch (Exception e) { throw new Exception("Error installing package " + packageFile, e); } } /// /// Temperary check to test that we support stylesheets /// /// private void PackageSupportedCheck(XElement rootElement) { XElement styleSheets = rootElement.Element(Constants.Packaging.StylesheetsNodeName); if (styleSheets != null && styleSheets.Elements().Any()) throw new NotSupportedException("Stylesheets is not suported in this version of umbraco"); } private static T[] EmptyArrayIfNull(object obj) { return obj == null ? new T[0] : null; } private static IEnumerable EmptyEnumerableIfNull(object obj) { return obj == null ? Enumerable.Empty() : null; } private XDocument GetConfigXmlDoc(string packageFilePath) { var configXmlContent = _packageExtraction.ReadTextFileFromArchive(packageFilePath, Constants.Packaging.PackageXmlFileName, out _); return XDocument.Parse(configXmlContent); } public XElement GetConfigXmlElement(string packageFilePath) { XDocument document = GetConfigXmlDoc(packageFilePath); if (document.Root == null || document.Root.Name.LocalName.Equals(Constants.Packaging.UmbPackageNodeName) == false) { throw new ArgumentException("xml does not have a root node called \"umbPackage\"", packageFilePath); } return document.Root; } internal void PackageStructureSanetyCheck(string packageFilePath) { XElement rootElement = GetConfigXmlElement(packageFilePath); PackageStructureSanetyCheck(packageFilePath, rootElement); } private void PackageStructureSanetyCheck(string packageFilePath, XElement rootElement) { XElement filesElement = rootElement.Element(Constants.Packaging.FilesNodeName); if (filesElement != null) { var sourceDestination = ExtractSourceDestinationFileInformation(filesElement).ToArray(); var missingFiles = _packageExtraction.FindMissingFiles(packageFilePath, sourceDestination.Select(i => i.Key)).ToArray(); if (missingFiles.Any()) { throw new Exception("The following file(s) are missing in the package: " + string.Join(", ", missingFiles.Select( mf => { var sd = sourceDestination.Single(fi => fi.Key == mf); return string.Format("source: \"{0}\" destination: \"{1}\"", sd.Key, sd.Value); }))); } IEnumerable dubletFileNames = _packageExtraction.FindDubletFileNames(packageFilePath).ToArray(); if (dubletFileNames.Any()) { throw new Exception("The following filename(s) are found more than one time in the package, since the filename is used ad primary key, this is not allowed: " + string.Join(", ", dubletFileNames)); } } } private static IEnumerable GetPackageActions(XElement actionsElement, string packageName) { if (actionsElement == null) { return new PackageAction[0]; } if (string.Equals(Constants.Packaging.ActionsNodeName, actionsElement.Name.LocalName) == false) { throw new ArgumentException("Must be \"" + Constants.Packaging.ActionsNodeName + "\" as root", "actionsElement"); } return actionsElement.Elements(Constants.Packaging.ActionNodeName) .Select(elemet => { XAttribute aliasAttr = elemet.Attribute(Constants.Packaging.AliasNodeNameCapital); if (aliasAttr == null) throw new ArgumentException( "missing \"" + Constants.Packaging.AliasNodeNameCapital + "\" atribute in alias element", "actionsElement"); var packageAction = new PackageAction { XmlData = elemet, Alias = aliasAttr.Value, PackageName = packageName, }; var attr = elemet.Attribute(Constants.Packaging.RunatNodeAttribute); if (attr != null && Enum.TryParse(attr.Value, true, out ActionRunAt runAt)) { packageAction.RunAt = runAt; } attr = elemet.Attribute(Constants.Packaging.UndoNodeAttribute); if (attr != null && bool.TryParse(attr.Value, out var undo)) { packageAction.Undo = undo; } return packageAction; }).ToArray(); } private IEnumerable InstallDocuments(XElement documentsElement, int userId = 0) { if ((string.Equals(Constants.Packaging.DocumentSetNodeName, documentsElement.Name.LocalName) == false) && (string.Equals(Constants.Packaging.DocumentsNodeName, documentsElement.Name.LocalName) == false)) { throw new ArgumentException("Must be \"" + Constants.Packaging.DocumentsNodeName + "\" as root", "documentsElement"); } if (string.Equals(Constants.Packaging.DocumentSetNodeName, documentsElement.Name.LocalName)) return _packagingService.ImportContent(documentsElement, -1, userId); return documentsElement.Elements(Constants.Packaging.DocumentSetNodeName) .SelectMany(documentSetElement => _packagingService.ImportContent(documentSetElement, -1, userId)) .ToArray(); } private IEnumerable InstallStylesheets(XElement styleSheetsElement) { if (string.Equals(Constants.Packaging.StylesheetsNodeName, styleSheetsElement.Name.LocalName) == false) { throw new ArgumentException("Must be \"" + Constants.Packaging.StylesheetsNodeName + "\" as root", "styleSheetsElement"); } // TODO: Call _packagingService when import stylesheets import has been implimentet if (styleSheetsElement.HasElements == false) { return new List(); } throw new NotImplementedException("The packaging service do not yes have a method for importing stylesheets"); } private IEnumerable InstallDocumentTypes(XElement documentTypes, int userId = 0) { if (string.Equals(Constants.Packaging.DocumentTypesNodeName, documentTypes.Name.LocalName) == false) { if (string.Equals(Constants.Packaging.DocumentTypeNodeName, documentTypes.Name.LocalName) == false) throw new ArgumentException( "Must be \"" + Constants.Packaging.DocumentTypesNodeName + "\" as root", "documentTypes"); documentTypes = new XElement(Constants.Packaging.DocumentTypesNodeName, documentTypes); } return _packagingService.ImportContentTypes(documentTypes, userId); } private IEnumerable InstallTemplats(XElement templateElement, int userId = 0) { if (string.Equals(Constants.Packaging.TemplatesNodeName, templateElement.Name.LocalName) == false) { throw new ArgumentException("Must be \"" + Constants.Packaging.TemplatesNodeName + "\" as root", "templateElement"); } return _packagingService.ImportTemplates(templateElement, userId); } private IEnumerable InstallFiles(string packageFilePath, XElement filesElement) { var sourceDestination = ExtractSourceDestinationFileInformation(filesElement); sourceDestination = AppendRootToDestination(FullPathToRoot, sourceDestination); _packageExtraction.CopyFilesFromArchive(packageFilePath, sourceDestination); return sourceDestination.Select(sd => sd.Value).ToArray(); } private KeyValuePair[] AppendRootToDestination(string fullpathToRoot, IEnumerable> sourceDestination) { return sourceDestination.Select( sd => new KeyValuePair(sd.Key, Path.Combine(fullpathToRoot, sd.Value))).ToArray(); } private IEnumerable InstallMacros(XElement macroElements, int userId = 0) { if (string.Equals(Constants.Packaging.MacrosNodeName, macroElements.Name.LocalName) == false) { throw new ArgumentException("Must be \"" + Constants.Packaging.MacrosNodeName + "\" as root", "macroElements"); } return _packagingService.ImportMacros(macroElements, userId); } private IEnumerable InstallDictionaryItems(XElement dictionaryItemsElement) { if (string.Equals(Constants.Packaging.DictionaryItemsNodeName, dictionaryItemsElement.Name.LocalName) == false) { throw new ArgumentException("Must be \"" + Constants.Packaging.DictionaryItemsNodeName + "\" as root", "dictionaryItemsElement"); } return _packagingService.ImportDictionaryItems(dictionaryItemsElement); } private IEnumerable InstallLanguages(XElement languageElement, int userId = 0) { if (string.Equals(Constants.Packaging.LanguagesNodeName, languageElement.Name.LocalName) == false) { throw new ArgumentException("Must be \"" + Constants.Packaging.LanguagesNodeName + "\" as root", "languageElement"); } return _packagingService.ImportLanguages(languageElement, userId); } private IEnumerable InstallDataTypes(XElement dataTypeElements, int userId = 0) { if (string.Equals(Constants.Packaging.DataTypesNodeName, dataTypeElements.Name.LocalName) == false) { if (string.Equals(Constants.Packaging.DataTypeNodeName, dataTypeElements.Name.LocalName) == false) { throw new ArgumentException("Must be \"" + Constants.Packaging.DataTypeNodeName + "\" as root", "dataTypeElements"); } } return _packagingService.ImportDataTypeDefinitions(dataTypeElements, userId); } private PreInstallWarnings GetPreInstallWarnings(XElement rootElement) { XElement files = rootElement.Element(Constants.Packaging.FilesNodeName); XElement styleSheets = rootElement.Element(Constants.Packaging.StylesheetsNodeName); XElement templates = rootElement.Element(Constants.Packaging.TemplatesNodeName); XElement alias = rootElement.Element(Constants.Packaging.MacrosNodeName); var sourceDestination = EmptyArrayIfNull>(files) ?? ExtractSourceDestinationFileInformation(files); var installWarnings = new PreInstallWarnings(); var macroAliases = EmptyEnumerableIfNull(alias) ?? ConflictingPackageData.FindConflictingMacros(alias); installWarnings.ConflictingMacroAliases = macroAliases; var templateAliases = EmptyEnumerableIfNull(templates) ?? ConflictingPackageData.FindConflictingTemplates(templates); installWarnings.ConflictingTemplateAliases = templateAliases; var stylesheetNames = EmptyEnumerableIfNull(styleSheets) ?? ConflictingPackageData.FindConflictingStylesheets(styleSheets); installWarnings.ConflictingStylesheetNames = stylesheetNames; installWarnings.UnsecureFiles = FindUnsecureFiles(sourceDestination); installWarnings.FilesReplaced = FindFilesToBeReplaced(sourceDestination); return installWarnings; } private KeyValuePair[] FindFilesToBeReplaced(IEnumerable> sourceDestination) { return sourceDestination.Where(sd => File.Exists(Path.Combine(FullPathToRoot, sd.Value))).ToArray(); } private KeyValuePair[] FindUnsecureFiles(IEnumerable> sourceDestinationPair) { return sourceDestinationPair.Where(sd => IsFileDestinationUnsecure(sd.Value)).ToArray(); } 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 KeyValuePair[] ExtractSourceDestinationFileInformation(XElement filesElement) { if (string.Equals(Constants.Packaging.FilesNodeName, filesElement.Name.LocalName) == false) { throw new ArgumentException("the root element must be \"Files\"", "filesElement"); } return filesElement.Elements(Constants.Packaging.FileNodeName) .Select(e => { XElement guidElement = e.Element(Constants.Packaging.GuidNodeName); if (guidElement == null) { throw new ArgumentException("Missing element \"" + Constants.Packaging.GuidNodeName + "\"", "filesElement"); } XElement orgPathElement = e.Element(Constants.Packaging.OrgPathNodeName); if (orgPathElement == null) { throw new ArgumentException("Missing element \"" + Constants.Packaging.OrgPathNodeName + "\"", "filesElement"); } XElement orgNameElement = e.Element(Constants.Packaging.OrgNameNodeName); if (orgNameElement == null) { throw new ArgumentException("Missing element \"" + Constants.Packaging.OrgNameNodeName + "\"", "filesElement"); } var fileName = PrepareAsFilePathElement(orgNameElement.Value); var relativeDir = UpdatePathPlaceholders(PrepareAsFilePathElement(orgPathElement.Value)); var relativePath = Path.Combine(relativeDir, fileName); return new KeyValuePair(guidElement.Value, relativePath); }).ToArray(); } private static string PrepareAsFilePathElement(string pathElement) { return pathElement.TrimStart(new[] {'\\', '/', '~'}).Replace("/", "\\"); } private MetaData GetMetaData(XElement xRootElement) { XElement infoElement = xRootElement.Element(Constants.Packaging.InfoNodeName); if (infoElement == null) { throw new ArgumentException("Did not hold a \"" + Constants.Packaging.InfoNodeName + "\" element", "xRootElement"); } var majorElement = infoElement.XPathSelectElement(Constants.Packaging.PackageRequirementsMajorXpath); var minorElement = infoElement.XPathSelectElement(Constants.Packaging.PackageRequirementsMinorXpath); var patchElement = infoElement.XPathSelectElement(Constants.Packaging.PackageRequirementsPatchXpath); var nameElement = infoElement.XPathSelectElement(Constants.Packaging.PackageNameXpath); var versionElement = infoElement.XPathSelectElement(Constants.Packaging.PackageVersionXpath); var urlElement = infoElement.XPathSelectElement(Constants.Packaging.PackageUrlXpath); var licenseElement = infoElement.XPathSelectElement(Constants.Packaging.PackageLicenseXpath); var authorNameElement = infoElement.XPathSelectElement(Constants.Packaging.AuthorNameXpath); var authorUrlElement = infoElement.XPathSelectElement(Constants.Packaging.AuthorWebsiteXpath); var readmeElement = infoElement.XPathSelectElement(Constants.Packaging.ReadmeXpath); XElement controlElement = xRootElement.Element(Constants.Packaging.ControlNodeName); return new MetaData { Name = StringValue(nameElement), Version = StringValue(versionElement), Url = StringValue(urlElement), License = StringValue(licenseElement), LicenseUrl = StringAttribute(licenseElement, Constants.Packaging.PackageLicenseXpathUrlAttribute), AuthorName = StringValue(authorNameElement), AuthorUrl = StringValue(authorUrlElement), Readme = StringValue(readmeElement), Control = StringValue(controlElement), ReqMajor = IntValue(majorElement), ReqMinor = IntValue(minorElement), ReqPatch = IntValue(patchElement) }; } private static string StringValue(XElement xElement, string defaultValue = "") { return xElement == null ? defaultValue : xElement.Value; } private static string StringAttribute(XElement xElement, string attribute, string defaultValue = "") { return xElement == null ? defaultValue : xElement.HasAttributes ? xElement.AttributeValue(attribute) : defaultValue; } private static int IntValue(XElement xElement, int defaultValue = 0) { return xElement == null ? defaultValue : int.TryParse(xElement.Value, out var val) ? val : defaultValue; } 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; } } }