using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Web.Hosting; using System.Xml.Linq; using System.Xml.XPath; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Packaging; using File = System.IO.File; namespace Umbraco.Core.Services { public class PackageInstallerService : IPackageInstallerService { private const string PACKAGE_XML_FILE_NAME = "package.xml"; private readonly IFileService _fileService; private readonly IMacroService _macroService; private readonly IPackagingService _packagingService; private readonly IUnpackHelper _unpackHelper; public PackageInstallerService(IPackagingService packagingService, IMacroService macroService, IFileService fileService, IUnpackHelper unpackHelper) { _packagingService = packagingService; if (unpackHelper != null) _unpackHelper = unpackHelper; else throw new ArgumentNullException("unpackHelper"); if (fileService != null) _fileService = fileService; else throw new ArgumentNullException("fileService"); if (macroService != null) _macroService = macroService; else throw new ArgumentNullException("macroService"); } public PackageInstallationSummary InstallPackageFile(string packageFilePath, int userId) { FileInfo fi = GetPackageFileInfo(packageFilePath); string tempDir = null; try { tempDir = _unpackHelper.UnPackToTempDirectory(fi.FullName); return InstallFromDirectory(tempDir, userId); } finally { if (string.IsNullOrEmpty(tempDir) == false && Directory.Exists(tempDir)) { Directory.Delete(tempDir, true); } } } public PackageMetaData GetMetaData(string packageFilePath) { var documentElement = GetConfigXmlDocFromPackageFile(packageFilePath); var rootElement = documentElement.Element("umbPackage"); if (rootElement == null) { throw new ArgumentException("xml does not have a root node called \"umbPackage\"", packageFilePath); } return GetMetaData(rootElement); } public PackageImportIssues FindPackageImportIssues(string packageFilePath) { var documentElement = GetConfigXmlDocFromPackageFile(packageFilePath); var rootElement = documentElement.Element("umbPackage"); if (rootElement == null) { throw new ArgumentException("File does not have a root node called \"umbPackage\"", packageFilePath); } return FindImportIssues(rootElement); } private FileInfo GetPackageFileInfo(string packageFilePath) { if (string.IsNullOrEmpty(packageFilePath)) { throw new ArgumentNullException("packageFilePath"); } var fi = new FileInfo(packageFilePath); if (fi.Exists == false) { throw new Exception("Error - file not found. Could find file named '" + packageFilePath + "'"); } // Check if the file is a valid package if (fi.Extension.Equals(".umb", StringComparison.InvariantCultureIgnoreCase) == false) { throw new Exception( "Error - file isn't a package (doesn't have a .umb extension). Check if the file automatically got named '.zip' upon download."); } return fi; } private XDocument GetConfigXmlDocFromPackageFile(string packageFilePath) { FileInfo packageFileInfo = GetPackageFileInfo(packageFilePath); string configXmlContent = _unpackHelper.ReadSingleTextFile(packageFileInfo.FullName, PACKAGE_XML_FILE_NAME); var packageConfig = XDocument.Parse(configXmlContent); return packageConfig; } private PackageInstallationSummary InstallFromDirectory(string packageDir, int userId) { var configXml = GetConfigXmlDocFromPackageDirectory(packageDir); var rootElement = configXml.XPathSelectElement("/umbPackage"); if (rootElement == null) { throw new ArgumentException("File does not have a root node called \"umbPackage\"", packageDir); } var dataTypes = rootElement.Element("DataTypes"); var languages = rootElement.Element("Languages"); var dictionaryItems = rootElement.Element("DictionaryItems"); var macroes = rootElement.Element("Macros"); var files = rootElement.Element("Files"); var templates = rootElement.Element("Templates"); var documentTypes = rootElement.Element("DocumentTypes"); var styleSheets = rootElement.Element("Stylesheets"); var documentSet = rootElement.Element("DocumentSet"); var actions = rootElement.Element("Actions"); return new PackageInstallationSummary { MetaData = GetMetaData(rootElement), DataTypesInstalled = dataTypes == null ? Enumerable.Empty() : InstallDataTypes(dataTypes, userId), LanguagesInstalled = languages == null ? Enumerable.Empty() : InstallLanguages(languages, userId), DictionaryItemsInstalled = dictionaryItems == null ? Enumerable.Empty() : InstallDictionaryItems(dictionaryItems, userId), MacrosInstalled = macroes == null ? Enumerable.Empty() : InstallMacros(macroes, userId), FilesInstalled = packageDir == null ? Enumerable.Empty>() : InstallFiles(packageDir, files), TemplatesInstalled = templates == null ? Enumerable.Empty() : InstallTemplats(templates, userId), DocumentTypesInstalled = documentTypes == null ? Enumerable.Empty() : InstallDocumentTypes(documentTypes, userId), StylesheetsInstalled = styleSheets == null ? Enumerable.Empty() : InstallStylesheets(styleSheets, userId), DocumentsInstalled = documentSet == null ? Enumerable.Empty() : InstallDocuments(documentSet, userId), PackageInstallActions = actions == null ? Enumerable.Empty>() : GetInstallActions(actions), PackageUninstallActions = actions == null ? string.Empty : GetUninstallActions(actions) }; } private static string GetUninstallActions(XElement actionsElement) { //saving the uninstall actions untill the package is uninstalled. return actionsElement.Elements("Action").Where(e => e.HasAttributes && e.Attribute("undo") != null && e.Attribute("undo").Value.Equals("false()", StringComparison.InvariantCultureIgnoreCase) == false) // SelectNodes("Actions/Action [@undo != false()]"); .Select(m => m.Value).Aggregate((workingSentence, next) => next + workingSentence); } private static IEnumerable> GetInstallActions(XElement actionsElement) { if (actionsElement == null) { return Enumerable.Empty>(); } if ("Actions".Equals(actionsElement.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { throw new ArgumentException("Must be \"Actions\" as root", "actionsElement"); } return actionsElement.Elements("Action") .Where( e => e.HasAttributes && (e.Attribute("runat") == null || e.Attribute("runat").Value.Equals("uninstall", StringComparison.InvariantCultureIgnoreCase) == false)) // .SelectNodes("Actions/Action [@runat != 'uninstall']") .Select(elemet => { var aliasAttr = elemet.Attribute("alias"); if (aliasAttr == null) throw new ArgumentException("missing alias atribute in alias element", "actionsElement"); return new {elemet, alias = aliasAttr.Value}; }).ToDictionary(x => x.alias, x => x.elemet); } private IEnumerable InstallDocuments(XElement documentsElement, int userId = 0) { if ("DocumentSet".Equals(documentsElement.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { throw new ArgumentException("Must be \"DocumentSet\" as root", "documentsElement"); } return _packagingService.ImportContent(documentsElement, -1, userId).Select(c => c.Id); } private IEnumerable InstallStylesheets(XElement styleSheetsElement, int userId = 0) { if ("Stylesheets".Equals(styleSheetsElement.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { throw new ArgumentException("Must be \"Stylesheets\" as root", "styleSheetsElement"); } return _packagingService.ImportStylesheets(styleSheetsElement, userId).Select(f => f.Id); } private IEnumerable InstallDocumentTypes(XElement documentTypes, int userId = 0) { if ("DocumentTypes".Equals(documentTypes.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { if ("DocumentType".Equals(documentTypes.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) throw new ArgumentException("Must be \"DocumentTypes\" as root", "documentTypes"); documentTypes = new XElement("DocumentTypes", documentTypes); } return _packagingService.ImportContentTypes(documentTypes, userId).Select(ct => ct.Id); } private IEnumerable InstallTemplats(XElement templateElement, int userId = 0) { if ("Templates".Equals(templateElement.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { throw new ArgumentException("Must be \"Templates\" as root", "templateElement"); } return _packagingService.ImportTemplates(templateElement, userId).Select(t => t.Id); } private static IEnumerable> InstallFiles(string packageDir, XElement filesElement) { if ("Files".Equals(filesElement.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { throw new ArgumentException("root element must be \"Files\"", "filesElement"); } string basePath = HostingEnvironment.ApplicationPhysicalPath; var xmlNodeList = filesElement.Elements("file"); return xmlNodeList.Select(e => { var orgPathElement = e.Element("orgPath"); if (orgPathElement == null) { throw new ArgumentException("Missing element \"orgPath\"", "filesElement"); } var guidElement = e.Element("guid"); if (guidElement == null) { throw new ArgumentException("Missing element \"guid\"", "filesElement"); } var orgNameElement = e.Element("orgName"); if (orgNameElement == null) { throw new ArgumentException("Missing element \"orgName\"", "filesElement"); } var destPath = GetFileName(basePath, orgPathElement.Value); var sourceFile = GetFileName(packageDir, guidElement.Value); var destFile = GetFileName(destPath, orgNameElement.Value); if (Directory.Exists(destPath) == false) Directory.CreateDirectory(destPath); var existingOverrided = File.Exists(destFile); File.Copy(sourceFile, destFile, true); return new KeyValuePair(orgPathElement.Value + "/" + orgNameElement.Value, existingOverrided); }); } private IEnumerable InstallMacros(XElement macroElements, int userId = 0) { if ("Macros".Equals(macroElements.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { throw new ArgumentException("Must be \"Templates\" as root", "macroElements"); } return _packagingService.ImportMacros(macroElements, userId).Select(m => m.Id); } private IEnumerable InstallDictionaryItems(XElement dictionaryItemsElement, int userId = 0) { if ("DictionaryItems".Equals(dictionaryItemsElement.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { throw new ArgumentException("Must be \"Templates\" as root", "dictionaryItemsElement"); } return _packagingService.ImportDictionaryItems(dictionaryItemsElement, userId).Select(di => di.Id); } private IEnumerable InstallLanguages(XElement languageElement, int userId = 0) { if ("Languages".Equals(languageElement.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { throw new ArgumentException("Must be \"Templates\" as root", "languageElement"); } return _packagingService.ImportLanguage(languageElement, userId).Select(l => l.Id); } private IEnumerable InstallDataTypes(XElement dataTypeElements, int userId = 0) { if ("DataTypes".Equals(dataTypeElements.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { if ("DataType".Equals(dataTypeElements.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { throw new ArgumentException("Must be \"Templates\" as root", "dataTypeElements"); } } return _packagingService.ImportDataTypeDefinitions(dataTypeElements, userId).Select(e => e.Id); } private static XDocument GetConfigXmlDocFromPackageDirectory(string packageDir) { string packageXmlPath = Path.Combine(packageDir, PACKAGE_XML_FILE_NAME); if (File.Exists(packageXmlPath) == false) { throw new FileNotFoundException("Could not find " + PACKAGE_XML_FILE_NAME + " in package"); } return XDocument.Load(packageXmlPath); } private PackageImportIssues FindImportIssues(XElement rootElement) { var files = rootElement.Element("Files"); var styleSheets = rootElement.Element("Stylesheets"); var templates = rootElement.Element("Templates"); var alias = rootElement.Element("Macros"); var packageImportIssues = new PackageImportIssues { UnsecureFiles = files == null ? Enumerable.Empty() : FindUnsecureFiles(files), ConflictingMacroAliases = alias == null ? Enumerable.Empty>() : FindConflictingMacroAliases(alias), ConflictingTemplateAliases = templates == null ? Enumerable.Empty>() : FindConflictingTemplateAliases(templates), ConflictingStylesheetNames = styleSheets == null ? Enumerable.Empty>() : FindConflictingStylesheetNames(styleSheets) }; return packageImportIssues; } private IEnumerable FindUnsecureFiles(XElement fileElement) { if ("Files".Equals(fileElement.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { throw new ArgumentException("the root element must be \"Files\"", "fileElement"); } return fileElement.Elements("file") .Where(FileNodeIsUnsecure) .Select(n => { var xElement = n.Element("orgName"); if (xElement == null) { throw new ArgumentException("missing a element: orgName", "n"); } return xElement.Value; }); } private IEnumerable> FindConflictingStylesheetNames(XElement stylesheetNotes) { if ("Stylesheets".Equals(stylesheetNotes.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { throw new ArgumentException("the root element must be \"Stylesheets\"", "stylesheetNotes"); } return stylesheetNotes.Elements("styleSheet") .Select(n => { var xElement = n.Element("Name"); if (xElement == null) { throw new ArgumentException("Missing \"Name\" element", "stylesheetNotes"); } string name = xElement.Name.LocalName; Stylesheet existingStilesheet = _fileService.GetStylesheetByName(name); // Don't know what to put in here... existing path whas the best i could come up with string existingFilePath = existingStilesheet == null ? null : existingStilesheet.Path; return new KeyValuePair(name, existingFilePath); }) .Where(kv => kv.Value != null); } private IEnumerable> FindConflictingTemplateAliases(XElement templateNotes) { if ("Templates".Equals(templateNotes.Name.LocalName, StringComparison.InvariantCultureIgnoreCase) == false) { throw new ArgumentException("Node must be a Templates node", "templateNotes"); } return templateNotes.Elements("Template") .Select(n => { var alias = n.Element("Alias"); if (alias == null) { throw new ArgumentException("missing a alias element", "templateNotes"); } string aliasStr = alias.Value; var existingTemplate = _fileService.GetTemplate(aliasStr) as Template; string existingName = existingTemplate == null ? null : existingTemplate.Name; return new KeyValuePair(aliasStr, existingName); }) .Where(kv => kv.Value != null); } private IEnumerable> FindConflictingMacroAliases(XElement macroNodes) { return macroNodes.Elements("macro") .Select(n => { var xElement = n.Element("alias"); if (xElement == null) { throw new ArgumentException("missing a alias element", "macroNodes"); } string alias = xElement.Value; IMacro macro = _macroService.GetByAlias(xElement.Value); string eksistingName = macro == null ? null : macro.Name; return new KeyValuePair(alias, eksistingName); }) .Where(kv => kv.Key != null && kv.Value != null); } private bool FileNodeIsUnsecure(XElement fileNode) { string basePath = HostingEnvironment.ApplicationPhysicalPath; var orgName = fileNode.Element("orgName"); if (orgName == null) { throw new ArgumentException("Missing element \"orgName\"", "fileNode"); } string destPath = GetFileName(basePath, orgName.Value); // Should be done with regex :) if (destPath.ToLower().Contains(IOHelper.DirSepChar + "app_code")) return true; if (destPath.ToLower().Contains(IOHelper.DirSepChar + "bin")) return true; return destPath.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase); } private PackageMetaData GetMetaData(XElement xRootElement) { XElement infoElement = xRootElement.Element("info"); if (infoElement == null) { throw new ArgumentException("Did not hold a \"info\" element", "xRootElement"); } var majorElement = infoElement.XPathSelectElement("/package/requirements/major"); var minorElement = infoElement.XPathSelectElement("/package/requirements/minor"); var patchElement = infoElement.XPathSelectElement("/package/requirements/patch"); var nameElement = infoElement.XPathSelectElement("/package/name"); var versionElement = infoElement.XPathSelectElement("/package/version"); var urlElement = infoElement.XPathSelectElement("/package/url"); var licenseElement = infoElement.XPathSelectElement("/package/license"); var authorNameElement = infoElement.XPathSelectElement("/author/name"); var authorUrlElement = infoElement.XPathSelectElement("/author/website"); var readmeElement = infoElement.XPathSelectElement("/readme"); var controlElement = xRootElement.Element("control"); int val; return new PackageMetaData { Name = nameElement == null ? string.Empty : nameElement.Value, Version = versionElement == null ? string.Empty : versionElement.Value, Url = urlElement == null ? string.Empty : urlElement.Value, License = licenseElement == null ? string.Empty : licenseElement.Value, LicenseUrl = licenseElement == null ? string.Empty : licenseElement.HasAttributes ? licenseElement.AttributeValue("url") : string.Empty, AuthorName = authorNameElement == null ? string.Empty : authorNameElement.Value, AuthorUrl = authorUrlElement == null ? string.Empty : authorUrlElement.Value, Readme = readmeElement == null ? string.Empty : readmeElement.Value, ReqMajor = majorElement == null ? 0 : int.TryParse(majorElement.Value, out val) ? val : 0, ReqMinor = minorElement == null ? 0 : int.TryParse(minorElement.Value, out val) ? val : 0, ReqPatch = patchElement == null ? 0 : int.TryParse(patchElement.Value, out val) ? val : 0, Control = controlElement == null ? string.Empty : controlElement.Value }; } /// /// Gets the name of the file in the specified path. /// Corrects possible problems with slashes that would result from a simple concatenation. /// Can also be used to concatenate paths. /// /// The path. /// Name of the file. /// The name of the file in the specified path. private static String GetFileName(String path, string fileName) { // virtual dir support fileName = IOHelper.FindFile(fileName); 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); } //to support virtual dirs we try to lookup the file... path = IOHelper.FindFile(path); Debug.Assert(path != null && path.Length >= 1); Debug.Assert(fileName != null && fileName.Length >= 1); path = path.Replace('/', '\\'); fileName = fileName.Replace('/', '\\'); // Does filename start with a slash? Does path end with one? bool fileNameStartsWithSlash = (fileName[0] == Path.DirectorySeparatorChar); bool pathEndsWithSlash = (path[path.Length - 1] == Path.DirectorySeparatorChar); // Path ends with a slash if (pathEndsWithSlash) { if (fileNameStartsWithSlash == false) // No double slash, just concatenate return path + fileName; return path + fileName.Substring(1); } if (fileNameStartsWithSlash) // Required slash specified, just concatenate return path + fileName; return path + Path.DirectorySeparatorChar + fileName; } } }