After a long tour on newsgroups, I am starting my blog today.
For my first entry, I am going to discuss a frequent question about IIS programmatic administration - how to manipulate the LIST data type (i.e. ScriptMaps, HttpErrors, ServerBindings, etc). No, MIMEMap is not included in this because it is not a LIST (it is its own custom data type) - good topic for a future blog...
The astute reader should realize that ADSUTIL.VBS already allows you to set/delete these LIST properties. However, ADSUTIL.VBS has a fatal flaw for multi-valued properties like LIST - it only allows you to set/delete the entire LIST. It does not allow you to replace one LIST item with a new value. Even worse, the default implementation only allows nine commandline parameters. So, suppose you wanted to:
THEY are all impossible do with an unmodified ADSUTIL.VBS. Therefore, I decided to write a little tool that illustrates how to:
In fact, the above tasks become very straight forward with this tool:
<chglist.vbs> W3SVC/1/root/ScriptMaps "" "*,%ProgramFiles%"WildcardISAPI.dll,0" /INSERT /COMMIT
<chglist.vbs> W3SVC/1/HttpErrors 404 404,*,URL,/CustomErrors/404.asp /COMMIT
<chglist.vbs> W3SVC/2/ServerBindings FIRST ":80:Host.domain.com" /INSERT /COMMIT
<chglist.vbs> W3SVC/ScriptMaps ".ext" "" /REMOVE /RECURSE /ALL /COMMIT
You can run the script with no commandline parameters for help, and operation should be self-explanatory. The tool does not make any changes unless you add /COMMIT, so feel free to poke around the various options. Feel free to post comments/suggestions as well as propose future topics.
[Updated 04/22/2006 - Added `` (double-backquote) syntax to put in double-quotes into a value. This allows manipulation of ScriptMaps, Application Extensions, etc pathnames with spaces.]
[Updated 05/11/2006 - Fixed bugs to make /recurse work correctly. Fixed bug when matching against "*" and /RegExp is not specified]
Enjoy.
//David
'
' Allows append/insert/remove of specific elements from an IIS "List" type node
' i.e. ScriptMap, HttpError, ServerBindings
'
' Origin : http://blogs.msdn.com/David.Wang/archive/2004/12/02/273681.aspx
' Version: December 1 2004
'
Option Explicit
On Error Resume Next
const ERROR_SUCCESS = 0
const ERROR_PATH_NOT_FOUND = 3
const ERROR_INVALID_PARAMETER = 87
const LIST_OP_FIRST = "FIRST"
const LIST_OP_LAST = "LAST"
const LIST_OPTION_REPLACE = 0
const LIST_OPTION_INSERT = 1
const LIST_OPTION_REMOVE = 2
const LIST_OPTION_ALL = 4
const LIST_OPTION_RECURSE = 8
Dim CRLF
CRLF = CHR(13) & CHR(10)
Dim strHelp
strHelp = "Edit/Replace IIS metabase LIST properties" & CRLF &_
CRLF &_
WScript.ScriptName & " PropertyPath ExistValue NewValue [Options]" & CRLF &_
CRLF &_
"Where:" & CRLF &_
" PropertyPath IIS metabase property path whose data type is LIST." & CRLF &_
" i.e. W3SVC/ScriptMaps, W3SVC/HttpErrors" & CRLF &_
" ExistValue Value to case-insensitive literal match against existing" & CRLF &_
" LIST elements." & CRLF &_
" FIRST - matches the first LIST element." & CRLF &_
" LAST - matches the last LIST element." & CRLF &_
" NewValue New value that replaces the matched the LIST element." & CRLF &_
"Options:" & CRLF &_
" /INSERT Insert before LIST element matching ." & CRLF &_
" /REMOVE Remove LIST element matching ." & CRLF &_
" /ALL Operate on ALL matching . Default is first match." & CRLF &_
" /REGEXP Use as RegExp to match. Default is literal." & CRLF &_
" /RECURSE Recursively perform the operation underneath ." & CRLF &_
" /VERBOSE Give more status/output." & CRLF &_
" /COMMIT Actually perform changes. Default only shows." & CRLF &_
""
dim Debug
Debug = true
dim Verbose
Verbose = false
dim reMatch
reMatch = false
Dim strServer
Dim strNamespace
Dim strSchemaNamespace
Dim strNodeSyntax
Dim objNode
Dim nOperationType
Dim strNormalizedPath
Dim strPropertyPath
Dim strPropertyName
Dim strPropertyExistValue
Dim strPropertyNewValue
Dim i,j
'
' Start of script
'
strServer = "localhost"
strNamespace = "IIS://" & strServer
strSchemaNamespace = strNamespace & "/" & "Schema"
'
' Parse the commandline
'
If WScript.Arguments.Count < 3 Then
Err.Number = ERROR_INVALID_PARAMETER
HandleError "Insufficient number of arguments." & CRLF &_
CRLF &_
strHelp &_
""
End If
nOperationType = LIST_OPTION_REPLACE
For i = 0 To WScript.Arguments.Count - 1
Select Case UCase( WScript.Arguments( i ) )
Case "/INSERT"
nOperationType = nOperationType Or LIST_OPTION_INSERT
Case "/REMOVE"
nOperationType = nOperationType Or LIST_OPTION_REMOVE
Case "/ALL"
nOperationType = nOperationType Or LIST_OPTION_ALL
Case "/RECURSE"
nOperationType = nOperationType Or LIST_OPTION_RECURSE
Case "/COMMIT"
Debug = false
Case "/VERBOSE"
Verbose = true
Case "/REGEXP"
reMatch = true
Case Else
If ( i = 0 ) Then
'
' Split out PropertyName and its ParentPath from PropertyPath
'
Err.Clear
strNormalizedPath = NormalizePath( WScript.Arguments( 0 ) )
HandleError "Failed to normalize PropertyPath."
j = InstrRev( strNormalizedPath, "/", -1, 0 )
If ( j = 0 Or j = 1 ) Then
Err.Number = ERROR_PATH_NOT_FOUND
HandleError "Invalid PropertyPath."
End If
Err.Clear
strPropertyPath = NormalizePath( Mid( strNormalizedPath, 1, j - 1 ) )
HandleError "Failed to retrieve/normalize PropertyPath."
Err.Clear
strPropertyName = NormalizePath( Mid( strNormalizedPath, j + 1 ) )
HandleError "Failed to retrieve/normalize PropertyName."
ElseIf ( i = 1 ) Then
'
' The existing match value
'
strPropertyExistValue = Replace( UCase( WScript.Arguments( 1 ) ), "``", """" )
ElseIf ( i = 2 ) Then
'
' The new replace value
'
strPropertyNewValue = Replace( WScript.Arguments( 2 ), "``", """" )
Else
Err.Number = ERROR_INVALID_PARAMETER
HandleError "Unknown parameter " & WScript.Arguments( i ) & CRLF &_
CRLF &_
strHelp &_
""
End If
End Select
Next
LogVerbose "OpType = " & nOperationType
LogVerbose "PropertyPath = " & strPropertyPath
LogVerbose "PropertyName = " & strPropertyName
LogVerbose "ExistValue = " & strPropertyExistValue
LogVerbose "NewValue = " & strPropertyNewValue
'
' Check the data type for the given property
' If it is not LIST, do not process any further
'
Err.Clear
Set objNode = GetObject( strSchemaNamespace & "/" & strPropertyName )
HandleError "Cannot read schema for property " & strPropertyName
strNodeSyntax = UCase( objNode.Syntax )
LogVerbose "Syntax = " & strNodeSyntax
LogVerbose ""
Select Case strNodeSyntax
Case "LIST"
'
' Finally, we are ready to do some real work
'
Err.Clear
Err.Number = HandleListOps( nOperationType, strPropertyPath, strPropertyName, strPropertyExistValue, strPropertyNewValue, ( nOperationType And LIST_OPTION_RECURSE ) <> 0 )
HandleError ""
Case Else
Err.Clear
Err.Number = ERROR_PATH_NOT_FOUND
HandleError "Cannot handle " & strPropertyPath & "/" & strPropertyName & " with type " & strNodeSyntax
End Select
'
' End of script
'
'
' Sub routines and functions
'
Sub HandleError( errorDescription )
If ( Err.Number <> 0 ) Then
If ( IsEmpty( errorDescription ) ) Then
LogEcho Err.Description
Else
LogEcho errorDescription
End If
WScript.Quit Err.Number
End If
End Sub
Function NormalizePath( strInput )
'
' Replace all " with /
'
strInput = Replace( strInput, """, "/", 1, -1 )
'
' Replace all // with /
'
Do
strInput = Replace( strInput, "//", "/", 1, -1 )
Loop While ( Instr( strInput, "//" ) <> 0 )
'
' Removing leading and trailing /
'
If ( Left( strInput, 1 ) = "/" ) Then
strInput = Right( strInput, Len( strInput ) - 1 )
End If
If ( Right( strInput, 1 ) = "/" ) Then
strInput = Left( strInput, Len( strInput ) - 1 )
End If
NormalizePath = strInput
End Function
Function HandleListOps( OpType, strPropertyPath, strPropertyName, strPropertyExistValue, strPropertyNewValue, bRecurse )
On Error Resume Next
Dim objNode, objNodeAttribute
Dim objList
Dim objElement
Dim objNewArray
Dim PerformedOperation
Dim Operation
Dim re
Dim reMatched
Dim i, j
Err.Clear
Set objNode = GetObject( strNamespace & "/" & strPropertyPath )
objList = objNode.Get( strPropertyName )
If ( Err.Number <> 0 Or IsEmpty( objList ) ) Then
LogEcho "Failed to retrieve " & strPropertyPath & "/" & strPropertyName
HandleListOps = Err.Number
Exit Function
End If
Err.Clear
Set objNodeAttribute = objNode.GetPropertyAttribObj(strPropertyName)
HandleError "Failed to retrieve Attributes for " & strPropertyPath & "/" & strPropertyName
If ( objNodeAttribute.IsInherit = true ) Then
LogEcho strPropertyPath & "/" & strPropertyName & " (Inherited)"
If ( bRecurse = true ) Then
LogEcho( "Ignoring inherited property for Recursive Modification" )
Exit Function
End If
Else
LogEcho strPropertyPath & "/" & strPropertyName
End If
'
' j is the count of elements in objNewArray
' So that we can resize it to the right size in the end
'
j = 0
'
' Size objNewArray to maximum possible size up-front, later shrink it
'
Redim objNewArray( UBound( objList ) + UBound( objList ) + 1 )
'
' PerformedOperation indicates whether something has matched and already
' operated upon, in this session. Start with 'not yet' = 0
'
PerformedOperation = 0
'
' Setup the RegExp match based on the existing value to search for
'
Set re = new RegExp
re.Pattern = strPropertyExistValue
re.IgnoreCase = true
re.Global = true
'
' Do this test outside of IF conditional because on error resume next
' turns off failures due to incorrect Pattern
'
Err.Clear
reMatched = re.Test( objElement )
If ( Err.Number <> 0 Or reMatch = false ) Then
reMatched = false
End If
LogVerbose "Original:"
For i = LBound( objList ) To UBound( objList )
objElement = objList( i )
'LogVerbose i & "(" & j & ")" & ": " & objElement
If ( ( ( ( strPropertyExistValue = LIST_OP_FIRST ) And ( i = LBound( objList ) ) ) Or _
( ( strPropertyExistValue = LIST_OP_LAST ) And ( i = UBound( objList ) ) ) Or _
( ( reMatch = false ) And ( Instr( UCase( objElement ), strPropertyExistValue ) > 0 ) ) Or _
( reMatched = true ) _
) _
And _
( ( ( OpType And LIST_OPTION_ALL ) <> 0 ) Or ( PerformedOperation = 0 ) ) _
) Then
Operation = "Replace "
If ( ( OpType And LIST_OPTION_REMOVE ) <> 0 ) Then
'Don't copy this element for deletion
Operation = "Remove "
Else
objNewArray( j ) = strPropertyNewValue
j = j + 1
If ( ( OpType And LIST_OPTION_INSERT ) <> 0 ) Then
Operation = "Insert "
objNewArray( j ) = objElement
j = j + 1
End If
End If
PerformedOperation = 1
Else
Operation = ""
objNewArray( j ) = objElement
j = j + 1
End If
LogVerbose Operation & objElement
Next
'
' Resize the final array to the correct size prior to SetInfo
'
ReDim Preserve objNewArray( j - 1 )
LogVerbose "New:"
For i = LBound( objNewArray ) To UBound( objNewArray )
LogDebug i & ": " & objNewArray( i )
Next
If ( Debug = false ) Then
If ( PerformedOperation = 1 ) Then
Err.Clear
objNode.Put strPropertyName, objNewArray
objNode.SetInfo
HandleError "Failed to SetInfo " & strPropertyPath & "/" & strPropertyName
LogEcho "SUCCESS: Updated " & strPropertyPath & "/" & strPropertyName
Else
LogEcho "SUCCESS: Nothing to update"
End If
Else
If ( PerformedOperation = 1 ) Then
LogEcho "DEBUG: Matched. Did not SetInfo"
Else
LogEcho "SUCCESS: No Match. Did not SetInfo"
End If
End If
If ( bRecurse = true ) Then
For Each objElement In objNode
LogEcho ""
HandleListOps = HandleListOps( OpType, NormalizePath( Mid( objElement.AdsPath, Len( strNamespace ) + 1 ) ), strPropertyName, strPropertyExistValue, strPropertyNewValue, bRecurse )
Next
End If
HandleListOps = 0
End Function
Sub LogEcho( str )
WScript.Echo str
End Sub
Sub LogDebug( str )
If ( Debug = true ) Then
LogEcho str
End If
End Sub
Sub LogVerbose( str )
If ( Verbose = true ) Then
LogEcho str
End If
End Sub