// This code is provided under the Microsoft Public License, reproduced below. // // This license governs use of the accompanying software. If you use the software, you // accept this license. If you do not accept the license, do not use the software. // // 1. Definitions // The terms "reproduce," "reproduction," "derivative works," and "distribution" have the // same meaning here as under U.S. copyright law. // A "contribution" is the original software, or any additions or changes to the software. // A "contributor" is any person that distributes its contribution under this license. // "Licensed patents" are a contributor's patent claims that read directly on its contribution. // // 2. Grant of Rights // (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. // (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. // // 3. Conditions and Limitations // (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. // (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically. // (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software. // (D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license. // (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. // *** GENERAL SUPPORT CONSTANTS START *** // Name of this custom action in MSI database var LogMessage_CustomActionName = "CheckPage_UpdateSelected"; var MsiEscapedLeftBracket = "[\\[]"; var MsiEscapedRightBracket = "[\\]]"; var ErrorCode_PathNotFound = -2146828212; var ErrorCode_FileNotFound = -2146828235; var ClassName_WScriptUtil = "WScript.Shell"; var ClassName_FileSystem = "Scripting.FileSystemObject"; // Used internally by Registry_* methods var RegistryHiveCode = { HKEY_CLASSES_ROOT : 0x80000000, HKEY_CURRENT_USER : 0x80000001, HKEY_LOCAL_MACHINE : 0x80000002, HKEY_USERS : 0x80000003, HKEY_CURRENT_CONFIG : 0x80000005 }; // Session.Message var MsgKind = { Error : 0x01000000, Warning : 0x02000000, User : 0x03000000, Log : 0x04000000 }; // FileSystemObject.OpenTextFile var FileSystemIoMode = { ForReading : 1, ForWriting : 2, ForAppending : 8 }; // FileSystemObject.OpenTextFile var FileSystemFormat = { TextModeDefault : -2, TextModeUnicode : -1, TextModeASCII : 0 }; // Separator for items in a list property var PropertyList_ItemSeparator = "<*>"; // Separator for fields within an item in a list property var PropertyList_FieldSeparator = ""; // Separator for items in a list property var PropertyList_ItemSeparator = "<*>"; // Separator for fields within an item in a list property var PropertyList_FieldSeparator = ""; // PROPERTIES // List of items to display on CheckPage - keyed by description field from PropertyName_DetectedThings var PropertyName_SourceList = "CHECKPAGE_SOURCELIST"; // List of checked items in PropertyName_SourceList var PropertyName_SelectedList = "CHECKPAGE_SELECTEDLIST"; // PropertyName_SourceList element number of first displayed item var PropertyName_CheckPage_DisplayRangeStart= "CHECKPAGE_DISPLAYRANGESTART"; // PropertyName_SourceList element number of last displayed item (not necessarilly on last checkbox) var PropertyName_CheckPage_DisplayRangeEnd = "CHECKPAGE_DISPLAYRANGEEND"; // Current page number var PropertyName_DisplayRange_CurrPageNo = "CHECKPAGE_CURRPAGENO"; // Number of pages of items var PropertyName_DisplayRange_NoPages = "CHECKPAGE_NOPAGES"; // Number of the highest page that exists, or 0 if no pages var PropertyName_DisplayRange_LastPage = "CHECKPAGE_LASTPAGENO"; // Other var Format_PropertyNameItemLabel = "CHECKPAGE_DISPLAYITEM{0}_LABEL"; var Format_PropertyNameItemChecked = "CHECKPAGE_DISPLAYITEM{0}_CHECKED"; var PropertyName_DetectedThings = "DETECTED_THINGS"; String.prototype.format = function() { var formatted = this; for (currArgNo = 0; currArgNo < arguments.length; currArgNo++) formatted = formatted.replace("{" +currArgNo.toString()+ "}", arguments[currArgNo]); return formatted; }; var CheckPageInfo = new CheckPageInfo(PropertyName_SourceList, PropertyName_SelectedList, PropertyName_CheckPage_DisplayRangeStart, PropertyName_CheckPage_DisplayRangeEnd, PropertyName_DisplayRange_CurrPageNo, PropertyName_DisplayRange_NoPages, PropertyName_DisplayRange_LastPage, Format_PropertyNameItemLabel, Format_PropertyNameItemChecked); // *** GENERAL SUPPORT CONSTANTS END *** // This single script file contains all script methods used for all 5 CAs. Each CA should contain a complete copy of this // file, where only the content between GENERAL SUPPORT CONSTANTS END and GENERAL SUPPORT FUNCTIONS START is different. // This makes it easy to keep shared functions in sync. That CA-specific content is relatively small. What you need for // each CA is listed below. NOTE this was extracted from a working project and was not tested on it's own, so you may // encounter a minor error or two due to this. // *** FOR LOCATE *** // You may be able to omit this CA // In my case, I needed to detect a list of installed software and track much more than just a string for each. // So I built an object to hold this information, returned the detection results in an array of those objects, and then // serialized it to/from a single string property via the PropertyList_ methods. // // If a single string is sufficient information for each element in the checklist, you can omit this CA. // Otherwise: // Create an object to store the fields // Detect what you're looking for and return it as an array of objects // Serialize the objects to a string via PropertyList_ // Save the serialized list to a string (Session.Property(PropertyName_DetectedThings) = PropertyList_FromObjectArray(detectedThings); // *** FOR INITIALIZE *** // If a single string is sufficient information for each information in the checklist, then just store that in SourceList Session.Property(PropertyName_SourceList) = ["ExampleA", "ExampleB", "ExampleC", "ExampleD", "ExampleE", "ExampleF", "ExampleG", "ExampleH", "ExampleI" ]; // Otherwise: // 1. Read in the property storing the list of objects from Locate // 2. Deserialize the object array from the string PropertyList_ToObjectArray(Session.Property(PropertyName_DetectedThings), new MyObject()); // 3. For each element of the array, store their unique descriptive name in a string, separated by ItemSeparator // 4. Store that string in SourceList // // In both cases, you must then call CheckPage_Initialize CheckPage_Initialize(CheckPageInfo); // *** FOR UPDATESELECTED *** CheckPage_UpdateSelected(CheckPageInfo); // *** FOR PREVIOUSPAGE *** CheckPage_AdjustCurrentPage(CheckPageInfo, -1); // *** FOR NEXTPAGE *** CheckPage_AdjustCurrentPage(CheckPageInfo, 1); // *** GENERAL SUPPORT FUNCTIONS START *** // Creates a CheckPageInfo object that describes an instance of CheckPage functionality // 1. To display these properties on the dialog: // a. Use a TextBox, Edit, or Checkbox control. Other controls may not work. // b. Set the property field equal to the property being displayed in the field. If using more than one property, updates are only guaranteed when the listed property is changed, so specify a property that always changes when any of the referenced ones do - it does not have to be one of the properties used in the control's Text field. // c. Change the value of the property in a CA set to run when a button is clicked. // d. The UI buttons that run the CA in C must have a CA for each property referenced that sets the property equal to itself to force MSI to pickup the new value. This must be a special type of Dialog SetProperty custom action where the name is the property name in squared brackets and the argument is just the property name. These must be sequenced after the value-change CA in C. No other type of MSI CA can get MSI to pickup the changed property value in the GUI silently. If displaying UI is OK, the CAs to present a modal dialog or change the displayed dialog will cause MSI to pickup the new properties, but that is generally not what's wanted here. function CheckPageInfo( newSourceListPropertyName, newSelectedListPropertyName, newDisplayRangeStartPropertyName, newDisplayRangeEndPropertyName, newDisplayRangeCurrPageNo, newDisplayRangeNoPages, newDisplayLastPageNo, newItemLabelPropertyNameFormat, newItemCheckedPropertyNameFormat ) { this.sourceListPropertyName = newSourceListPropertyName; this.selectedListPropertyName = newSelectedListPropertyName; this.displayRangeStartPropertyName = newDisplayRangeStartPropertyName; this.displayRangeEndPropertyName = newDisplayRangeEndPropertyName; this.displayRangeCurrPageNo = newDisplayRangeCurrPageNo; this.displayRangeNoPages = newDisplayRangeNoPages; this.displayLastPageNo = newDisplayLastPageNo; this.itemLabelPropertyNameFormat = newItemLabelPropertyNameFormat; this.itemCheckedPropertyNameFormat = newItemCheckedPropertyNameFormat; } // Initializes the GUI check page dialog. The property sourceListPropertyName must be initialized with the list of items // for the GUI check page. function CheckPage_Initialize( checkPage ) { var sourceList = []; var noPages = 0; // Clear the CheckPage state for (currDisplayItemNo = 0; currDisplayItemNo < 8; currDisplayItemNo++) { // Construct the names of the properties for the current item currDisplayItemProperty_Label = checkPage.itemLabelPropertyNameFormat.format(currDisplayItemNo.toString()); currDisplayItemProperty_Checked = checkPage.itemCheckedPropertyNameFormat.format(currDisplayItemNo.toString()); Session.Property(currDisplayItemProperty_Label) = ""; Session.Property(currDisplayItemProperty_Checked) = ""; } // Clear the selections and range Session.Property(checkPage.selectedListPropertyName) = ""; Session.Property(checkPage.displayRangeCurrPageNo) = "0"; Session.Property(checkPage.displayRangeNoPages) = "0"; Session.Property(checkPage.displayLastPageNo) = "0"; // Initialize page/displayed range sourceList = PropertyList_BreakApart(Session.Property(checkPage.sourceListPropertyName), PropertyList_ItemSeparator); if (sourceList.length > 0) { noPages = Math.round((sourceList.length/8) + 0.5); Session.Property(checkPage.displayRangeNoPages) = noPages.toString(); Session.Property(checkPage.displayLastPageNo) = (noPages-1).toString(); Session.Property(checkPage.displayRangeStartPropertyName) = "0"; if (sourceList.length <= 8) Session.Property(checkPage.displayRangeEndPropertyName) = (sourceList.length - 1).toString(); else Session.Property(checkPage.displayRangeEndPropertyName) = "7"; } // Setup starting display CheckPage_UpdateDisplayedItems(checkPage); } // Initialize GUI item display, based on the current page to display function CheckPage_UpdateDisplayedItems( checkPage ) { var startSourceItemNo = 0; var lastSourceItemNo = 0; var currSourceItemNo = 0; var currDisplayItemNo = 0; var sourceList = []; var selectedList = []; var currDisplayItemProperty_Label = ""; var currDisplayItemProperty_Checked = ""; // Load MSI state sourceList = PropertyList_BreakApart(Session.Property(checkPage.sourceListPropertyName), PropertyList_ItemSeparator); selectedList = PropertyList_BreakApart(Session.Property(checkPage.selectedListPropertyName), PropertyList_ItemSeparator); startSourceItemNo = parseInt(Session.Property(checkPage.displayRangeStartPropertyName)); lastSourceItemNo = parseInt(Session.Property(checkPage.displayRangeEndPropertyName)); // Set the description and checked state for each display item currSourceItemNo = startSourceItemNo; for (currDisplayItemNo = 0; currDisplayItemNo < 8; currDisplayItemNo++) { // Construct the names of the properties for the current item currDisplayItemProperty_Label = checkPage.itemLabelPropertyNameFormat.format(currDisplayItemNo.toString()); currDisplayItemProperty_Checked = checkPage.itemCheckedPropertyNameFormat.format(currDisplayItemNo.toString()); // Set the label and checked properties to their new values if (currSourceItemNo < sourceList.length) { Session.Property(currDisplayItemProperty_Label) = sourceList[currSourceItemNo]; if (ListContainsItem(selectedList, sourceList[currSourceItemNo])) Session.Property(currDisplayItemProperty_Checked) = "1"; else Session.Property(currDisplayItemProperty_Checked) = ""; } else { Session.Property(currDisplayItemProperty_Label) = ""; Session.Property(currDisplayItemProperty_Checked) = ""; } currSourceItemNo += 1; } } // Changes currPageNo by the adjustmentValue specified, or does nothing if can't move back or forward, respectively function CheckPage_AdjustCurrentPage( checkPage, adjustmentValue ) { var sourceList = []; var currPageNo = 0; var displaySourceItemNoStart = 0; var displaySourceItemNoEnd = 0; var nextPageNo = 0; var nextDisplayRangeStart = 0; var nextDisplayRangeEnd = 0; // Read in state sourceList = PropertyList_BreakApart(Session.Property(checkPage.sourceListPropertyName), PropertyList_ItemSeparator); displaySourceItemNoStart = parseInt(Session.Property(checkPage.displayRangeStartPropertyName)); displaySourceItemNoEnd = parseInt(Session.Property(checkPage.displayRangeEndPropertyName)); noPages = parseInt(Session.Property(checkPage.displayRangeNoPages)); currPageNo = parseInt(Session.Property(checkPage.displayRangeCurrPageNo)); nextPageNo = currPageNo + adjustmentValue; if (sourceList.length > 0) { if ((nextPageNo < noPages) && (nextPageNo >= 0)) { nextDisplayRangeStart = nextPageNo*8; nextDisplayRangeEnd = Math.round((nextPageNo+1)*8-1); Session.Property(checkPage.displayRangeStartPropertyName) = nextDisplayRangeStart.toString(); if (nextDisplayRangeEnd < sourceList.length) { Session.Property(checkPage.displayRangeEndPropertyName) = nextDisplayRangeEnd.toString(); } else { nextDisplayRangeEnd = sourceList.length - 1; Session.Property(checkPage.displayRangeEndPropertyName) = nextDisplayRangeEnd.toString(); } // Used only by the GUI to determine when to advance to next dialog or show next page of items Session.Property(checkPage.displayRangeCurrPageNo) = nextPageNo.toString(); // Refresh the GUI CheckPage_UpdateDisplayedItems(checkPage); } } else { Session.Property(checkPage.displayRangeStartPropertyName) = "0"; Session.Property(checkPage.displayRangeEndPropertyName) = "0"; } } // Updates selectedList from the GUI function CheckPage_UpdateSelected( checkPage ) { var sourceList = []; var selectedList = []; var currPageNo = 0; var currDisplayItemNo = 0; var currDisplayItemProperty_Label = ""; var currDisplayItemProperty_Checked = ""; // Read in current state sourceList = StringList_StringToArray(Session.Property(checkPage.sourceListPropertyName)); selectedList = StringList_StringToArray(Session.Property(checkPage.selectedListPropertyName)); currDisplayItemName = ""; currDisplayItemSelectedIndex= -1; if (sourceList.length > 0) { // Map each display item to the selected list and add/remove to/from that list according to checked state for (currDisplayItemNo = 0; currDisplayItemNo < 8; currDisplayItemNo++) { // Construct the names of the properties for the current display item currDisplayItemProperty_Label = checkPage.itemLabelPropertyNameFormat.format(currDisplayItemNo.toString()); currDisplayItemProperty_Checked = checkPage.itemCheckedPropertyNameFormat.format(currDisplayItemNo.toString()); // Get the key of the current item and determine if it's in the selected list currDisplayItemName = Session.Property(currDisplayItemProperty_Label); currDisplayItemSelectedIndex = ListFindItem(selectedList, currDisplayItemName); if (Session.Property(currDisplayItemProperty_Checked) != "") { if (currDisplayItemSelectedIndex == -1) selectedList.push(currDisplayItemName); } else { if (currDisplayItemSelectedIndex != -1) selectedList.splice(currDisplayItemSelectedIndex, 1); } } // Update persistent store of selected items Session.Property(checkPage.selectedListPropertyName) = StringList_ArrayToString(selectedList); } } // Returns TRUE if sourceList contains itemToFind, else FALSE function ListContainsItem( sourceList, itemToFind ) { return (ListFindItem(sourceList, itemToFind) != -1); } // Searches sourceList for itemToFind and returns the element number if found, else -1 function ListFindItem( sourceList, itemToFind ) { var currItemNo = 0; var foundItem = false; for (currItemNo = 0; currItemNo < sourceList.length; currItemNo++) { if (sourceList[currItemNo] == itemToFind) { foundItem = true; break; } } if (foundItem) return currItemNo; else return -1; } // Returns an array of substrings of sourceString separated by itemSeparator function PropertyList_BreakApart( sourceString, itemSeparator ) { var substringArray = []; var currSubstringStartPos = 0; var separatorPos = 0; separatorPos = sourceString.indexOf(itemSeparator, currSubstringStartPos); while ((separatorPos >= 0) && (separatorPos < sourceString.length)) { substringArray.push(sourceString.substr(currSubstringStartPos, separatorPos - currSubstringStartPos)); currSubstringStartPos = separatorPos + itemSeparator.length; separatorPos = sourceString.indexOf(itemSeparator, currSubstringStartPos); } if (currSubstringStartPos <= (sourceString.length-1)) substringArray.push(sourceString.substr(currSubstringStartPos)); return substringArray; } // Serializes an array of strings to a single string function StringList_ArrayToString( stringArray ) { var packedString = ""; var currElementNo = 0; for (currElementNo = 0; currElementNo < stringArray.length; currElementNo++) { if (packedString == "") packedString = stringArray[currElementNo]; else packedString = packedString +PropertyList_ItemSeparator+ stringArray[currElementNo]; } return packedString; } // Deserializes a string array serialized with StringList_ArrayToString function StringList_StringToArray( sourceString ) { return PropertyList_BreakApart(sourceString, PropertyList_ItemSeparator); } // Appends pathToAppend onto basePath and returns the result function AppendPath( basePath, pathToAppend ) { var fileUtil = SafeCreateObject(ClassName_FileSystem); return fileUtil.BuildPath(basePath, pathToAppend); } // Creates an ActiveX object of type className and returns it or NULL // Logs any failure using the name of the calling function function SafeCreateObject( className ) { var newObject = null; try { newObject = new ActiveXObject(className); } catch (errorInfo) { // Log error info LogMessage(SafeCreateObject.caller +" exception creating object: " +GetErrorInfoString(errorInfo)); } return newObject; } // Gets a 1-line description of the specified Error object // Format: Error (): function GetErrorInfoString( errorInfo ) { var description = ""; if (errorInfo.name != "") description = "Error "+ errorInfo.number.toString() +" ("+ errorInfo.name +"): " +errorInfo.description; else description = "Error "+ errorInfo.number.toString() +": " +errorInfo.description; return description; } // Returns the name of the passed function object function GetFunctionName( functionObject ) { var nameFinder = /function (.*?)\(/ var matchGroups = nameFinder.exec(functionObject.toString()); return matchGroups[1]; } // Logs an exception using the name of the caller, in the format : [:] Error (): // descriptionPrefix is optional function LogExceptionFromCaller( errorInfo, descriptionPrefix ) { var logLine = GetFunctionName(LogExceptionFromCaller.caller) +": "; if (descriptionPrefix == undefined) logLine = logLine + GetErrorInfoString(errorInfo); else logLine = logLine +descriptionPrefix+ ": " +GetErrorInfoString(errorInfo); LogMessage(logLine); } // Logs [