// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. namespace WixToolset.Harvesters { using System; using System.Collections; using System.Diagnostics; using System.Globalization; using System.IO; using WixToolset.Harvesters.Extensibility; using Wix = WixToolset.Harvesters.Serialize; /// /// The template type. /// public enum TemplateType { /// /// A fragment template. /// Fragment, /// /// A module template. /// Module, /// /// A product template. /// Package } /// /// The mutator for the WiX Toolset Internet Information Services Extension. /// public sealed class UtilMutator : BaseMutatorExtension { private ArrayList components; private ArrayList componentGroups; private string componentGroupName; private bool createFragments; private ArrayList directories; private ArrayList directoryRefs; private ArrayList files; private ArrayList features; private SortedList fragments; private bool autogenerateGuids; private bool generateGuids; private string guidFormat = "B"; // Defaults to guid in {} private Wix.IParentElement rootElement; private bool setUniqueIdentifiers; private TemplateType templateType; /// /// Instantiate a new UtilMutator. /// public UtilMutator() { this.components = new ArrayList(); this.componentGroups = new ArrayList(); this.directories = new ArrayList(); this.directoryRefs = new ArrayList(); this.features = new ArrayList(); this.files = new ArrayList(); this.fragments = new SortedList(); } /// /// Gets or sets the value of the component group name. /// /// The component group name. public string ComponentGroupName { get { return this.componentGroupName; } set { this.componentGroupName = value; } } /// /// Gets or sets the option to create fragments. /// /// The option to create fragments. public bool CreateFragments { get { return this.createFragments; } set { this.createFragments = value; } } /// /// Gets or sets the option to autogenerate component guids at compile time. /// /// The option to autogenerate component guids. public bool AutogenerateGuids { get { return this.autogenerateGuids; } set { this.autogenerateGuids = value; } } /// /// Gets or sets the option to generate missing guids. /// /// The option to generate missing guids. public bool GenerateGuids { get { return this.generateGuids; } set { this.generateGuids = value; } } /// /// Gets or sets the option to set the format of guids. /// D - 32 digits separated by hyphens: /// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx /// B - 32 digits separated by hyphens, enclosed in brackets: /// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} /// /// Guid format either B or D. public string GuidFormat { get { return this.guidFormat; } set { this.guidFormat = value; } } /// /// Gets the sequence of the extension. /// /// The sequence of the extension. public override int Sequence { get { return 1000; } } /// /// Gets of sets the option to set unique identifiers. /// /// The option to set unique identifiers. public bool SetUniqueIdentifiers { get { return this.setUniqueIdentifiers; } set { this.setUniqueIdentifiers = value; } } /// /// Gets or sets the template type. /// /// The template type. public TemplateType TemplateType { get { return this.templateType; } set { this.templateType = value; } } /// /// Mutate a WiX document. /// /// The Wix document element. public override void Mutate(Wix.Wix wix) { this.components.Clear(); this.directories.Clear(); this.directoryRefs.Clear(); this.features.Clear(); this.files.Clear(); this.fragments.Clear(); this.rootElement = null; // index elements in this wix document this.IndexElement(wix); this.MutateWix(wix); this.MutateFiles(); this.MutateDirectories(); this.MutateComponents(); if (null != this.componentGroupName) { this.CreateComponentGroup(wix); } // add the components to the product feature after all the identifiers have been set if (TemplateType.Package == this.templateType) { var feature = (Wix.Feature)this.features[0]; foreach (Wix.ComponentGroup group in this.componentGroups) { var componentGroupRef = new Wix.ComponentGroupRef(); componentGroupRef.Id = group.Id; feature.AddChild(componentGroupRef); } } else if (TemplateType.Module == this.templateType) { foreach (Wix.ISchemaElement element in wix.Children) { if (element is Wix.Module) { foreach (Wix.ComponentGroup group in this.componentGroups) { var componentGroupRef = new Wix.ComponentGroupRef(); componentGroupRef.Id = group.Id; ((Wix.IParentElement)element).AddChild(componentGroupRef); } break; } } } //if(!this.createFragments && TemplateType.Package foreach (Wix.Fragment fragment in this.fragments.Values) { wix.AddChild(fragment); } } /// /// Creates a component group with a given name. /// /// The Wix document element. private void CreateComponentGroup(Wix.Wix wix) { var componentGroup = new Wix.ComponentGroup(); componentGroup.Id = this.componentGroupName; this.componentGroups.Add(componentGroup); var cgFragment = new Wix.Fragment(); cgFragment.AddChild(componentGroup); wix.AddChild(cgFragment); var componentCount = 0; for (; componentCount < this.components.Count; componentCount++) { var c = this.components[componentCount] as Wix.Component; if (this.createFragments) { if (c.ParentElement is Wix.Directory) { var parentDirectory = c.ParentElement as Wix.Directory; componentGroup.AddChild(c); c.Directory = parentDirectory.Id; parentDirectory.RemoveChild(c); } else if (c.ParentElement is Wix.DirectoryRef || c.ParentElement is Wix.StandardDirectory) { var parentDirectory = c.ParentElement as Wix.DirectoryBase; componentGroup.AddChild(c); c.Directory = parentDirectory.Id; parentDirectory.RemoveChild(c); // Remove whole fragment if moving the component to the component group just leaves an empty DirectoryRef if (0 < this.fragments.Count && parentDirectory.ParentElement is Wix.Fragment) { var parentFragment = parentDirectory.ParentElement as Wix.Fragment; var childCount = 0; foreach (Wix.ISchemaElement element in parentFragment.Children) { childCount++; } // Component should always have an Id but the SortedList creation allows for null and bases the name on the fragment count which we cannot reverse engineer here. if (1 == childCount && !String.IsNullOrEmpty(c.Id)) { var removeIndex = this.fragments.IndexOfKey(String.Concat("Component:", c.Id)); if (0 <= removeIndex) { this.fragments.RemoveAt(removeIndex); } } } } } else { var componentRef = new Wix.ComponentRef(); componentRef.Id = c.Id; componentGroup.AddChild(componentRef); } } } /// /// Index an element. /// /// The element to index. private void IndexElement(Wix.ISchemaElement element) { if (element is Wix.Component) { this.components.Add(element); } else if (element is Wix.ComponentGroup) { this.componentGroups.Add(element); } else if (element is Wix.Directory) { this.directories.Add(element); } else if (element is Wix.DirectoryRef) { this.directoryRefs.Add(element); } else if (element is Wix.Feature) { this.features.Add(element); } else if (element is Wix.File) { this.files.Add(element); } else if (element is Wix.Module || element is Wix.PatchCreation || element is Wix.Package) { Debug.Assert(null == this.rootElement); this.rootElement = (Wix.IParentElement)element; } // index the child elements if (element is Wix.IParentElement parentElement) { foreach (Wix.ISchemaElement childElement in parentElement.Children) { this.IndexElement(childElement); } } } /// /// Mutate the components. /// private void MutateComponents() { var identifierGenerator = new IdentifierGenerator("Component", this.Core); if (TemplateType.Module == this.templateType) { identifierGenerator.MaxIdentifierLength = IdentifierGenerator.MaxModuleIdentifierLength; } foreach (Wix.Component component in this.components) { if (null == component.Id) { var firstFileId = String.Empty; // attempt to create a possible identifier from the first file identifier in the component foreach (Wix.File file in component[typeof(Wix.File)]) { firstFileId = file.Id; break; } if (String.IsNullOrEmpty(firstFileId)) { firstFileId = this.GetGuid(); } component.Id = identifierGenerator.GetIdentifier(firstFileId); } if (null == component.Guid) { if (this.AutogenerateGuids) { component.Guid = "*"; } else { component.Guid = this.GetGuid(); } } if (this.createFragments && component.ParentElement is Wix.Directory directory) { // parent directory must have an identifier to create a reference to it if (null == directory.Id) { break; } if (this.rootElement is Wix.Module) { // add a ComponentRef for the Component var componentRef = new Wix.ComponentRef(); componentRef.Id = component.Id; this.rootElement.AddChild(componentRef); } // create a new Fragment var fragment = new Wix.Fragment(); this.fragments.Add(String.Concat("Component:", component.Id ?? this.fragments.Count.ToString()), fragment); // create a new DirectoryRef var directoryRef = DirectoryHelper.CreateDirectoryReference(directory.Id); fragment.AddChild(directoryRef); // move the Component from the the Directory to the DirectoryRef directory.RemoveChild(component); directoryRef.AddChild(component); } } } /// /// Mutate the directories. /// private void MutateDirectories() { if (!this.setUniqueIdentifiers) { // assign all identifiers before fragmenting (because fragmenting requires them all to be present) var identifierGenerator = new IdentifierGenerator("Directory", this.Core); if (TemplateType.Module == this.templateType) { identifierGenerator.MaxIdentifierLength = IdentifierGenerator.MaxModuleIdentifierLength; } foreach (Wix.Directory directory in this.directories) { if (null == directory.Id) { directory.Id = identifierGenerator.GetIdentifier(directory.Name); } } } if (this.createFragments) { foreach (Wix.Directory directory in this.directories) { if (directory.ParentElement is Wix.Directory) { var parentDirectory = (Wix.DirectoryBase)directory.ParentElement; // parent directory must have an identifier to create a reference to it if (null == parentDirectory.Id) { return; } // create a new Fragment var fragment = new Wix.Fragment(); this.fragments.Add(String.Concat("Directory:", ("TARGETDIR" == directory.Id ? null : (null != directory.Id ? directory.Id : this.fragments.Count.ToString()))), fragment); // create a new DirectoryRef var directoryRef = DirectoryHelper.CreateDirectoryReference(parentDirectory.Id); fragment.AddChild(directoryRef); // move the Directory from the parent Directory to DirectoryRef parentDirectory.RemoveChild(directory); directoryRef.AddChild(directory); } else if (directory.ParentElement is Wix.Fragment parent) { // When creating fragments, remove any top-level Directory elements; // the fragments should be pulled in by their DirectoryRefs instead. parent.RemoveChild(directory); // Remove the fragment if it is empty. if (parent.Children.GetEnumerator().Current == null && parent.ParentElement != null) { ((Wix.IParentElement)parent.ParentElement).RemoveChild(parent); } } else if (directory.ParentElement == this.rootElement) { // create a new Fragment var fragment = new Wix.Fragment(); this.fragments.Add(String.Concat("Directory:", ("TARGETDIR" == directory.Id ? null : (null != directory.Id ? directory.Id : this.fragments.Count.ToString()))), fragment); // move the Directory from the root element to the Fragment this.rootElement.RemoveChild(directory); fragment.AddChild(directory); } } } } /// /// Mutate the files. /// private void MutateFiles() { var identifierGenerator = new IdentifierGenerator("File", this.Core); if (TemplateType.Module == this.templateType) { identifierGenerator.MaxIdentifierLength = IdentifierGenerator.MaxModuleIdentifierLength; } foreach (Wix.File file in this.files) { if (null == file.Id) { file.Id = identifierGenerator.GetIdentifier(Path.GetFileName(file.Source)); } } } /// /// Mutate a Wix element. /// /// The Wix element to mutate. private void MutateWix(Wix.Wix wix) { if (TemplateType.Fragment != this.templateType) { if (null != this.rootElement || 0 != this.features.Count) { throw new Exception("The template option cannot be used with Feature, Package, or Module elements present."); } // create a package element although it won't always be used var package = new Wix.SummaryInformation(); if (TemplateType.Module == this.templateType) { package.Id = this.GetGuid(); } else { package.Compressed = Wix.YesNoType.yes; } package.InstallerVersion = 200; var targetDir = new Wix.Directory(); targetDir.Id = "TARGETDIR"; targetDir.Name = "SourceDir"; foreach (Wix.DirectoryRef directoryRef in this.directoryRefs) { if (String.Equals(directoryRef.Id, "TARGETDIR", StringComparison.OrdinalIgnoreCase)) { var parent = directoryRef.ParentElement as Wix.IParentElement; foreach (Wix.ISchemaElement element in directoryRef.Children) { targetDir.AddChild(element); } parent.RemoveChild(directoryRef); if (null != ((Wix.ISchemaElement)parent).ParentElement) { var i = 0; foreach (Wix.ISchemaElement element in parent.Children) { i++; } if (0 == i) { var supParent = (Wix.IParentElement)((Wix.ISchemaElement)parent).ParentElement; supParent.RemoveChild((Wix.ISchemaElement)parent); } } break; } } if (TemplateType.Module == this.templateType) { var module = new Wix.Module(); module.Id = "PUT-MODULE-NAME-HERE"; module.Language = "1033"; module.Version = "1.0.0.0"; package.Manufacturer = "PUT-COMPANY-NAME-HERE"; module.AddChild(package); module.AddChild(targetDir); wix.AddChild(module); this.rootElement = module; } else // product { var product = new Wix.Package(); product.Id = this.GetGuid(); product.Language = "1033"; product.Manufacturer = "PUT-COMPANY-NAME-HERE"; product.Name = "PUT-PRODUCT-NAME-HERE"; product.UpgradeCode = this.GetGuid(); product.Version = "1.0.0.0"; product.AddChild(package); product.AddChild(targetDir); var media = new Wix.Media(); media.Id = "1"; media.Cabinet = "product.cab"; media.EmbedCab = Wix.YesNoType.yes; product.AddChild(media); var feature = new Wix.Feature(); feature.Id = "ProductFeature"; feature.Title = "PUT-FEATURE-TITLE-HERE"; feature.Level = 1; product.AddChild(feature); this.features.Add(feature); wix.AddChild(product); this.rootElement = product; } } } /// /// Get a generated guid or a placeholder for a guid. /// /// A generated guid or placeholder. private string GetGuid() { if (this.generateGuids) { return Guid.NewGuid().ToString(this.guidFormat, CultureInfo.InvariantCulture).ToUpper(CultureInfo.InvariantCulture); } else { return "PUT-GUID-HERE"; } } } }