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.Packaging.Models; using Umbraco.Core.Services; namespace Umbraco.Core.Packaging { internal class PackageInstallation : IPackageInstallation { private readonly IFileService _fileService; private readonly IMacroService _macroService; private readonly IPackagingService _packagingService; private IConflictingPackageContentFinder _conflictingPackageContentFinder; private readonly IPackageExtraction _packageExtraction; public PackageInstallation(IPackagingService packagingService, IMacroService macroService, IFileService fileService, IPackageExtraction packageExtraction) { 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"); } public IConflictingPackageContentFinder ConflictingPackageContentFinder { private get { return _conflictingPackageContentFinder ?? (_conflictingPackageContentFinder = new ConflictingPackageContentFinder(_macroService, _fileService)); } set { if (_conflictingPackageContentFinder != null) { throw new PropertyConstraintException("This property already have a value"); } _conflictingPackageContentFinder = value; } } private string _fullpathToRoot; public string FullpathToRoot { private get { return _fullpathToRoot ?? (_fullpathToRoot = GlobalSettings.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 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); 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 = EmptyArrayIfNull(dataTypes) ?? InstallDataTypes(dataTypes, userId); installationSummary.DataTypesInstalled = dataTypeDefinitions; var languagesInstalled = EmptyArrayIfNull(languages) ?? InstallLanguages(languages, userId); installationSummary.LanguagesInstalled = languagesInstalled; var dictionaryInstalled = EmptyArrayIfNull(dictionaryItems) ?? InstallDictionaryItems(dictionaryItems); installationSummary.DictionaryItemsInstalled = dictionaryInstalled; var macros = EmptyArrayIfNull(macroes)?? InstallMacros(macroes, userId); installationSummary.MacrosInstalled = macros; var keyValuePairs = EmptyArrayIfNull>(packageFile) ?? InstallFiles(packageFile, files); installationSummary.FilesInstalled = keyValuePairs; var templatesInstalled = EmptyArrayIfNull(templates) ?? InstallTemplats(templates, userId); installationSummary.TemplatesInstalled = templatesInstalled; var documentTypesInstalled = EmptyArrayIfNull(documentTypes) ?? InstallDocumentTypes(documentTypes, userId); installationSummary.DocumentTypesInstalled =documentTypesInstalled; var stylesheetsInstalled = EmptyArrayIfNull(styleSheets) ?? InstallStylesheets(styleSheets, userId); installationSummary.StylesheetsInstalled = stylesheetsInstalled; var documentsInstalled = EmptyArrayIfNull(documentSet) ?? InstallDocuments(documentSet, userId); installationSummary.DocumentsInstalled = documentsInstalled; var packageActions = EmptyArrayIfNull(actions) ?? GetPackageActions(actions, metaData.Name); installationSummary.Actions = packageActions; 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 XDocument GetConfigXmlDoc(string packageFilePath) { string filePathInPackage; string configXmlContent = _packageExtraction.ReadTextFileFromArchive(packageFilePath, Constants.Packaging.PackageXmlFileName, out filePathInPackage); return XDocument.Parse(configXmlContent); } private 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) { IEnumerable extractFileInPackageInfos = ExtractFileInPackageInfos(filesElement).ToArray(); IEnumerable missingFiles = _packageExtraction.FindMissingFiles(packageFilePath, extractFileInPackageInfos.Select(i => i.FileNameInPackage)).ToArray(); if (missingFiles.Any()) { throw new Exception("The following file(s) are missing in the package: " + string.Join(", ", missingFiles.Select( mf => { FileInPackageInfo fileInPackageInfo = extractFileInPackageInfos.Single(fi => fi.FileNameInPackage == mf); return string.Format("Guid: \"{0}\" Original File: \"{1}\"", fileInPackageInfo.FileNameInPackage, fileInPackageInfo.RelativePath); }))); } 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 PackageAction[] 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, }; XAttribute attr = elemet.Attribute(Constants.Packaging.RunatNodeAttribute); ActionRunAt runAt; if (attr != null && Enum.TryParse(attr.Value, true, out runAt)) { packageAction.RunAt = runAt; } attr = elemet.Attribute(Constants.Packaging.UndoNodeAttribute); bool undo; if (attr != null && bool.TryParse(attr.Value, out undo)) { packageAction.Undo = undo; } return packageAction; }).ToArray(); } private IContent[] InstallDocuments(XElement documentsElement, int userId = 0) { if (string.Equals(Constants.Packaging.DocumentSetNodeName, documentsElement.Name.LocalName) == false) { throw new ArgumentException("Must be \"" + Constants.Packaging.DocumentSetNodeName + "\" as root", "documentsElement"); } return _packagingService.ImportContent(documentsElement, -1, userId).ToArray(); } private IStylesheet[] InstallStylesheets(XElement styleSheetsElement, int userId = 0) { if (string.Equals(Constants.Packaging.StylesheetsNodeName, styleSheetsElement.Name.LocalName) == false) { throw new ArgumentException("Must be \"" + Constants.Packaging.StylesheetsNodeName + "\" as root", "styleSheetsElement"); } return _packagingService.ImportStylesheets(styleSheetsElement, userId).ToArray(); } private IContentType[] 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).ToArray(); } private ITemplate[] 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).ToArray(); } private Details[] InstallFiles(string packageFilePath, XElement filesElement) { return ExtractFileInPackageInfos(filesElement).Select(fpi => { bool existingOverrided = _packageExtraction.CopyFileFromArchive(packageFilePath, fpi.FileNameInPackage, fpi.FullPath); return new Details() { Source = fpi.FileNameInPackage, Destination = fpi.FullPath, Status = existingOverrided ? InstallStatus.Overwridden : InstallStatus.Inserted }; }).ToArray(); } private IMacro[] 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).ToArray(); } private IDictionaryItem[] 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).ToArray(); } private ILanguage[] 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).ToArray(); } private IDataTypeDefinition[] 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).ToArray(); } 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 conflictingPackageContent = new PreInstallWarnings { UnsecureFiles = files == null ? new IFileInPackageInfo[0] : FindUnsecureFiles(files), ConflictingMacroAliases = alias == null ? new IMacro[0] : ConflictingPackageContentFinder.FindConflictingMacros(alias), ConflictingTemplateAliases = templates == null ? new ITemplate[0] : ConflictingPackageContentFinder.FindConflictingTemplates(templates), ConflictingStylesheetNames = styleSheets == null ? new IStylesheet[0] : ConflictingPackageContentFinder.FindConflictingStylesheets(styleSheets) }; return conflictingPackageContent; } private IFileInPackageInfo[] FindUnsecureFiles(XElement fileElement) { return ExtractFileInPackageInfos(fileElement) .Where(IsFileNodeUnsecure).Cast().ToArray(); } private bool IsFileNodeUnsecure(FileInPackageInfo fileInPackageInfo) { // Should be done with regex :) if (fileInPackageInfo.Directory.ToLower().Contains(IOHelper.DirSepChar + "app_code")) return true; if (fileInPackageInfo.Directory.ToLower().Contains(IOHelper.DirSepChar + "bin")) return true; string extension = Path.GetExtension(fileInPackageInfo.Directory); return extension.Equals(".dll", StringComparison.InvariantCultureIgnoreCase); } private IEnumerable ExtractFileInPackageInfos(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"); } return new FileInPackageInfo { FileNameInPackage = guidElement.Value, FileName = PrepareAsFilePathElement(orgNameElement.Value), RelativeDir = UpdatePathPlaceholders( PrepareAsFilePathElement(orgPathElement.Value)), DestinationRootDir = FullpathToRoot }; }).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) { int val; return xElement == null ? defaultValue : int.TryParse(xElement.Value, out 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("[$UMBRACOCLIENT]", SystemDirectories.UmbracoClient); path = path.Replace("[$CONFIG]", SystemDirectories.Config); path = path.Replace("[$DATA]", SystemDirectories.Data); } return path; } } public class FileInPackageInfo : IFileInPackageInfo { public string RelativePath { get { return Path.Combine(RelativeDir, FileName); } } public string FileNameInPackage { get; set; } public string RelativeDir { get; set; } public string DestinationRootDir { private get; set; } public string Directory { get { return Path.Combine(DestinationRootDir, RelativeDir); } } public string FullPath { get { return Path.Combine(DestinationRootDir, RelativePath); } } public string FileName { get; set; } } public interface IFileInPackageInfo { string RelativeDir { get; } string RelativePath { get; } string FileName { get; set; } } }