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:

  • add a wildcard application mapping (with long filepath)
  • replace the 404 Custom Error URL while preserving all other Custom Error definitions
  • add/edit a Host header while preserving all others
  • recursively remove an application mapping from everywhere

THEY are all impossible do with an unmodified ADSUTIL.VBS. Therefore, I decided to write a little tool that illustrates how to:

  • Navigate and enumerate through the IIS configuration namespace (/RECURSE)
  • Locate LIST data types (like ScriptMaps, HttpErrors, ServerBindings, etc) (SYNTAX)
  • Enumerate and Manipulate LIST data type

In fact, the above tasks become very straight forward with this tool:

  • Add a wildcard application mapping for website ID 1
    <chglist.vbs> W3SVC/1/root/ScriptMaps "" "*,%ProgramFiles%/WildcardISAPI.dll,0" /INSERT /COMMIT
  • Change the 404 Custom Error for website ID 1
    <chglist.vbs> W3SVC/1/HttpErrors 404 404,*,URL,/CustomErrors/404.asp /COMMIT
  • Add a Host header for Website ID 2 while preserving all others
    <chglist.vbs> W3SVC/2/ServerBindings FIRST ":80:Host.domain.com" /INSERT /COMMIT
  • Recursively remove application mapping for ".ext" extension
    <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 ExplicitOn Error Resume Nextconst ERROR_SUCCESS             = 0const ERROR_PATH_NOT_FOUND      = 3const ERROR_INVALID_PARAMETER   = 87const LIST_OP_FIRST             = "FIRST"const LIST_OP_LAST              = "LAST"const LIST_OPTION_REPLACE       = 0const LIST_OPTION_INSERT        = 1const LIST_OPTION_REMOVE        = 2const LIST_OPTION_ALL           = 4const LIST_OPTION_RECURSE       = 8Dim CRLFCRLF = CHR(13) & CHR(10)Dim strHelpstrHelp = "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 DebugDebug = truedim VerboseVerbose = falsedim reMatchreMatch = falseDim strServerDim strNamespaceDim strSchemaNamespaceDim strNodeSyntaxDim objNodeDim nOperationTypeDim strNormalizedPathDim strPropertyPathDim strPropertyNameDim strPropertyExistValueDim strPropertyNewValueDim i,j'' Start of script'strServer = "localhost"strNamespace = "IIS://" & strServerstrSchemaNamespace = 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 IfnOperationType = LIST_OPTION_REPLACEFor 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 SelectNextLogVerbose "OpType       = " & nOperationTypeLogVerbose "PropertyPath = " & strPropertyPathLogVerbose "PropertyName = " & strPropertyNameLogVerbose "ExistValue   = " & strPropertyExistValueLogVerbose "NewValue     = " & strPropertyNewValue'' Check the data type for the given property' If it is not LIST, do not process any further'Err.ClearSet objNode = GetObject( strSchemaNamespace & "/" & strPropertyName )HandleError "Cannot read schema for property " & strPropertyNamestrNodeSyntax = UCase( objNode.Syntax )LogVerbose "Syntax       = " & strNodeSyntaxLogVerbose ""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 " & strNodeSyntaxEnd 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 IfEnd SubFunction 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 = strInputEnd FunctionFunction 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 = 0End FunctionSub LogEcho( str )    WScript.Echo strEnd SubSub LogDebug( str )    If ( Debug = true ) Then        LogEcho str    End IfEnd SubSub LogVerbose( str )    If ( Verbose = true ) Then        LogEcho str    End IfEnd Sub