The inner workings of 3U
In "Managing User Accounts, Part 1," October 2003, http://www.exchangeadmin.com, InstantDoc ID 39802, I introduced you to my User Update Utility (3U), a lightweight, simple, yet powerful graphical utility for maintaining user accounts in an Active Directory (AD) domain. I created this tool for administering user accounts in an Exchange 2000 Serverenabled AD domain; however, I've since modified it so that it can administer user accounts in AD domains without Exchange 2000 (or Exchange Server 2003).
In Part 1, I described 3U's various files, preparations for running 3U, and running the utility. Then, I began an exploration of 3U's code by describing how the code connects to AD and builds the interface. In this concluding installment, I continue the code exploration by describing how 3U tackles user account administration by calling the various subroutines in UserUpdate.hta, Results.htm, and PropPage.htm.
3U's Inner Workings
UserUpdate.hta is 3U's entry point. After establishing a connection to AD and building the interface, 3U is ready for you to enter search criteria and click the Find button that UserUpdate.hta presents.
Clicking the Find button runs the FindScript subroutine, which Listing 1, page 2, shows. FindScript verifies that you've entered data in at least one field, as the code at callout A in Listing 1 shows. If you haven't entered data, UserUpdate.hta displays a message stating that you must enter search information in one or more fields. If you did enter data, FindScript calls the ADSISearch subroutine (which Listing 2, page 2, shows) and passes to the subroutine the names of the attributes that contain values. Remember the BuildElement subroutine from Part 1? The last parameter of that subroutine assigned the corresponding lDAPDisplayName to the Input tag's Name attribute. The FindScript subroutine searches the lDAPDisplayName stored in the name attribute for any fields that contain values, then passes the resulting list of attributes to the ADSISearch subroutine.
ADSISearch uses the Active Directory Service Interfaces (ADSI) OLE DB provider to query AD for all user accounts that match the criteria specified on the form. Callout A in Listing 2 shows the code for preparing and running the query. The subroutine stores the result set from this search operation in the strVals variable. ADSISearch compiles this result set by using a Do...Loop statement to iterate through the recordset, as the code at callout B in Listing 2 shows. Within the loop, the script uses the Fields collection to read the mandatory attributes stored in the recordset. The CheckOptValue function deals with optional attributes that might not be assigned to the user account; unassigned attributes return a Null object value (rather than an empty value) to the script. (The CheckOptValue function is short, so I haven't included a listing for it. To more closely review the function, look for it in the script section of UserUpdate.hta.) Inside either the Do...Loop statement or the CheckOptValue function, ADSISearch adds a pipe symbol (|) after each attribute value in the result set to separate one table cell from the next. At the end of each loop iteration, the subroutine appends a double pipe symbol (||) to designate the end of a row of data.
As the final portion of Listing 2 shows, ADSISearch uses the VBScript Split function to create an array of records. The Split function uses the double pipe symbol as the delimiter to separate each row of data. ADSISearch stores the array in the arrRecords array variable, then hands the array to the BuildTable subroutine.
Using the Dynamic HTML (DHTML) table object model, the BuildTable subroutine uses the array of rows to construct a table in the iframe. BuildTable begins by clearing any existing rows, including the table head, as the code at callout A in Web Listing 1 (http://www.exchangeadmin.com, InstantDoc ID 40071) shows. Next, the subroutine recreates the table head and inserts the cells in the table head. Then, BuildTable uses the arrRecords array to build the table rows and cells and populate the cells with values, as the code at callout B in Web Listing 1 shows. For each row in the result set, BuildRow calls the table object's insertRow method and sets various attributes of the row object. For each cell in the result set, BuildRow calls the row object's insertCell method and sets various attributes of each cell object. The subroutine then uses each cell object's innerText property to populate each cell with a value.
Each row's onClick event calls the IdentifyRow subroutine, which performs two tasks: It enables the Edit button, and it assigns a user account's distinguishedName attribute to the Edit button's name attribute. Then, when you click the Edit button, EditScript passes the value to the User Account Properties Page (PropPage.htm). The Edit button's onClick event is assigned the EditScript subroutine (which Listing 3 shows) to accomplish the task of loading PropPage.htm and passing it a value. Because each row's onDblclick event is assigned the EditScript subroutine, you can also double-click a highlighted row in the result set to run the EditScript subroutine.
EditScript is a short subroutine that loads a modeless dialog box. I use a modeless dialog box so that you aren't restricted to opening just one instance of the User Account Properties Page from UserUpdate.hta. Instead, you can, for example, open two instances of a user account's Properties page, make changes in both windows, click OK to save your changes, and return to UserUpdate.hta. When you view the properties of that user account again, you'll see all the changes you just made.
Simple subroutines are assigned to the onClick events of the other two buttons in UserUpdate.hta—Reset and Close. The Reset button calls the ResetScript subroutine, which clears all the fields and the iframe. The Close button calls the QuitScript subroutine, which closes UserUpdate.hta.
The 3U Properties Page
From a coding perspective, PropPage.htm is similar to UserUpdate.hta. Therefore, I describe only how PropPage.htm differs from UserUpdate.hta. Unlike UserUpdate.hta, PropPage.htm is an HTML file, so it doesn't contain an HTA:APPLICATION element. Because PropPage.htm isn't an HTML Application (HTA), I can't use the Window_Onload event to initially load the page. Instead, I assign the LoadPage subroutine to the body element's onLoad event. LoadPage builds the User Account Properties Page in much the same way that the Window_Onload subroutine does for UserUpdate.hta.
One subroutine call that isn't present in UserUpdate.hta's load sequence but is present in PropPage.htm is SetIndividualElement. The SetIndividualElement subroutine sets attributes for the page's Street text area and Country drop-down list box. Except for a few minor adjustments to attributes of the textArea element, this element operates similarly to the page's input elements. In contrast, the CountryNameSelect element (which is a drop-down list) works quite a bit differently from the rest of the elements on the page.
Using the XMLDOMDocument object's SelectNode method, the SetIndividualElements subroutine populates the Country drop-down list with country names that the iso3166.xml file contains. As I mentioned in Part 1, because of copyright restrictions, the version of iso3166.xml included with 3U is only a sample of the complete country name and code list available from the International Organization for Standardization (ISO). The following xml element in the main body of PropPage.htm specifies the sample iso3166 .xml file as the source file of the data:
The SetIndividualElements subroutine uses the DHTML document object's createElement method to create option elements for each country name. Then, it uses the add method of the select element to assign each option element to the Country drop-down list.
ADSISearch is the final subroutine that LoadPage calls. ADSISearch requires one input parameter: the distinguished name (DN) of the user account to search. The following line of code passes the DN of the selected user account to ADSISearch:
The Window.dialogArguments property contains the user account's DN, which the EditScript subroutine in UserUpdate.hta provided to the dialogArguments property. In Listing 3, notice that EditScript assigns the strDN variable to the second parameter of the window.showModelessDialog method. The strDN variable is initialized with the value of the distinguishedName attribute previously stored in the Edit button's name attribute.
Using the user account's DN, the ADSISearch subroutine queries the user account for the attributes that ultimately appear in PropPage.htm's fields. One of the attributes, mailNickname, is present only if an Exchange 2003 or Exchange 2000 installation has extended the AD schema. To make 3U useful in organizations that don't run Exchange 2003 or Exchange 2000, I added some error checking to ADSISearch, as the code at callout A in Web Listing 2 shows. If the first Lightweight Directory Access Protocol (LDAP) query fails because of a missing attribute, the subroutine attempts the query again but substitutes the cn attribute for the mailNickName attribute in the LDAP query dialect. The cn attribute simply serves as an LDAP query placeholder and doesn't appear in the User Account Properties Page. ADSISearch also displays a message in PropPage.htm to inform you that because Exchange 2003 or Exchange 2000 isn't installed, the EMail Alias field has been disabled. The EMail Alias field contains the mailNickName attribute's value.
Following a successful query of AD, ADSISearch assigns a value to each field in PropPage.htm, including the Country drop-down list. Two factors complicate this task's code. First, multivalued attributes—specifically, description and postOfficeBox—are returned to the subroutine as arrays. Second, for the Country drop-down list, the subroutine assigns a country name by selecting an option element that the selected element contains.
The code at callout B in Web Listing 2 shows how ADSISearch deals with multivalued attributes. The subroutine uses the VBScript IsArray function to identify array values, then uses the VBScript LBound function to assign the array's first value to the field. For several reasons, the subroutine assigns only one value in these multivalued attributes. First, the description attribute is defined as multivalued but operates as a single-valued attribute. Microsoft defined this attribute as multivalued for backward compatibility. The postOfficeBox attribute operates like a multivalued attribute and thus accepts multiple entries; however, if you view the P.O. Box field in the Microsoft Management Console (MMC) Active Directory Users and Computers snap-in, you'll see that it takes only one value. If you assign a value to the P.O. Box field in the Active Directory Users and Computers snap-in, the subroutine clears any existing entries contained in the postOfficeBox attribute and adds your entry. Therefore, although both attributes are defined as multivalued and return an array from an LDAP query, you can work with the attributes as though they're single-valued.
The code at callout C in Web Listing 2 shows how ADSISearch uses the user account's co attribute and the select element's SelectedIndex property to select the proper country name. If the co attribute isn't assigned to the user account in AD, the select element's selectedIndex is set to 0, which doesn't set the Country drop-down list to anything. If the co attribute is assigned to the user account, the value of the co attribute is compared with each option element's text property until a match is found. When a match occurs, the select element's selectedIndex property, which is 0-based, is set equal to 1 less than the matching index of the option element's value.
All existing attributes and their values now appear in the User Account Properties Page. The next step is for 3U to determine whether you've changed any of the values that appear in PropPage.htm. Within the ADSISearch subroutine, each form element that you can change has the DetectChanges subroutine assigned to its onChange event. The DetectChanges subroutine appends a pipe symbol to the id value of each changed element and stores the entire value in a hidden input element called ChangedFieldsInput.
Committing Changes to AD
When you click OK at the bottom of the User Account Properties Page, the onClick event assigned to that button runs the CommitChanges script, which Web Listing 3 shows. This subroutine updates the attributes changed on the form to the ADSI local property cache, then commits these changes to AD. Updating single-valued attributes simply involves calling the Put method of ADSI's IADs core interface to update modified values. If you clear a value on the form, CommitChanges calls the PutEx method with the clear control code, ADS_PROPERTY_CLEAR, to remove the attribute from the user account.
Earlier, I mentioned that the description and postOfficeBox attributes are multivalued. However, the CommitChanges subroutine updates both as if they're single-valued. This procedure works fine for the description attribute because it behaves like a single-valued attribute. However, updating the multivalued postOfficeBox attribute clears any existing entries and replaces them with the one value you specify on the form. This approach is by design. An important 3U goal was to make it update attributes in the same way that the Active Directory Users and Computers snap-in does. However, I left three lines of commented code in PropPage.htm in case you want to append to the postOfficeBox attribute rather than replace it with one value. Simply uncomment the lines of code appearing at callout A and callout C in Web Listing 3. Be aware that if you append 40 values to this attribute, it will be full for a particular user account and won't be able to accept additional values. You probably won't reach 40 values for any one user account, but you should understand this limitation before you take this alternative approach to updating the postOfficeBox attribute.
The trickiest element to update is the Country drop-down list. The code at callout B in Web Listing 3 shows how I deal with this element. The CommitChanges subroutine encounters the drop-down list when it finds the interface element that's assigned the c name property. If this item's value is empty, CommitChanges uses the PutEx method with the ADS_PROPERTY_CLEAR control code to remove both the c (country code) and co (country name) attributes from the user account. Otherwise, the script identifies the index value of the selected option. Next, it uses the XMLDOMDocument object's SelectNodes and SelectSingleNode methods to match the country name with the proper XML node in the iso3166.xml file. After the script locates the proper node, the SelectSingleNode method retrieves the country code associated with the specified country name. Finally, CommitChanges assigns the c attribute the retrieved country code and assigns the co attribute the assigned country name.
3U at Your Service
That was a lot to take in! But, in the spirit of providing a complete solution, I believe this powerful, lightweight tool will go a long way toward easing your user account administration—a key task in almost all networks.