?
Solved

Can I edit the IIS 6.0 metabase.xml file with XSLT?

Posted on 2005-03-01
22
Medium Priority
?
841 Views
Last Modified: 2013-11-18
Hey everybody,

What I'm trying to do may be a little above and beyond what XSLT should be used for, but I'm trying it regardless.

Since IIS 6.0 uses an XML file for its configuration, I decided to come up with an XSLT to modify it, since I may need to set up application pools for as many as 20 applications in my app servers, and continually doing it at every re-install gets old fast.

I figured I could use XSLT since:
1) It's an XML file, and
2) I don't usually program in .NET or Java these days, and I'd be able to come up with the XSLT quicker.

However, the metabase.xml has a lot of extra characters in their values (line feeds, carriage returns, etc.),
and it looks like IIS won't function without them. I did some basic XSLT that just made a copy of the original metabase.xml file, and it looks like IIS won't accept the new file, because the special characters didn't get copied over:

<xsl:template match="@*|node()">
      <xsl:copy>
            <xsl:apply-templates select="@*|node()" />
        </xsl:copy>    
</xsl:template>

Does anyone have some insight on how I could get use XSLT for this, even with all of these special characters?
All suggestions/comments are much appreciated, this would be a major time-saver if I could get something like this functioning.

A sample metabase file can be found here:
http://www.winnetmag.com/Files/11/22281/22281.zip

Thanks!
0
Comment
Question by:Inward_Spiral
[X]
Welcome to Experts Exchange

Add your voice to the tech community where 5M+ people just like you are talking about what matters.

  • Help others & share knowledge
  • Earn cash & points
  • Learn & ask questions
  • 12
  • 10
22 Comments
 
LVL 26

Expert Comment

by:rdcpro
ID: 13435540
You can add whitespace between tags in XSLT, but I don't think there is any way of inserting whitespace like that *within* a tag, but truthfully it's hard to see how IIS would care about whitespace within an XML tag...that is this:

      <Custom
            Name="MD_0"
            ID="0"
            Value="SMTP Server"
            Type="STRING"
            UserType="UNKNOWN_UserType"
            Attributes="NO_ATTRIBUTES"
      />

Should be functionally equivalent to:

<Custom Name="MD_0" ID="0" Value="SMTP Server" Type="STRING" UserType="UNKNOWN_UserType" Attributes="NO_ATTRIBUTES"/>

The only time I can think of where whitespace like this is important is if the XML needs to be canonicalized for digital signing.  But the canonical form is quite different than metabase.xml...


I wonder if it was the namespace that caused the problem, if it's not getting correctly sent to the output.

If you have a copy of XML Spy, you can try opening the metabase.xml in it, then select "prettyprint" to reformat the XML.  Or just try manually removing some of the spaces from an existing copy, and restart IIS to see if it chokes.  

I know you can't change some nodes in metabase.xml, because these are encrypted.  But you're wanting to change stuff like custom error messages and other setup info?

Oh, I would also be sure the BOM (byte order mark) is preserved, though I don't think IIS will have a problem if it's not there.  

I'll try it out on one of my VPCs...

Regards,
Mike Sharp
0
 
LVL 26

Accepted Solution

by:
rdcpro earned 2000 total points
ID: 13435658
No, it's not the whitespace...I used this Identity transform:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  xmlns:out="urn:microsoft-catalog:XML_Metabase_V54_0">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>

<xsl:template match="/ | node()">
      <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:apply-templates select="node()"/>
      </xsl:copy>
</xsl:template>

<xsl:template match="@*">
      <xsl:copy>
      </xsl:copy>
</xsl:template>

<!-- Templates to handle changes go here -->

<xsl:template match="@HttpCustomHeaders">
      <xsl:attribute name="HttpCustomHeaders">X-Powered-By: MikeSharp</xsl:attribute>
</xsl:template>


</xsl:stylesheet>


and it set the HttpCustomHeaders correctly...no problem.

If you modify tags other than attributes, be cautious of the namespace.

Also, make sure you stop IIS before you try to save the result of the transform.  I notice that after a little while, IIS refreshes the metabase.xml, and pretty prints the whole thing, too.  But my header change has persisted.

I think you have a great idea, actually.  You could store your configuration changes in a separate XML file that you retrieve via the XSLT document() function.  And, you could have a variety of XSLT for different purposes, and import/include their templates into your main identity transform.  

In fact, it's hard to believe it hasn't been done before.  I'll have to ask our build engineer how he does this.

Regards,
Mike Sharp
0
 

Author Comment

by:Inward_Spiral
ID: 13440230
Glad you like the idea...and I can see how HttpCustomHeaders would work, but what about attributes like HttpErrors?

The HttpErrors attribute has values separated by spacing and a new line:
            HttpErrors="400,*,FILE,C:\WINDOWS\help\iisHelp\common\400.htm
                  401,1,FILE,C:\WINDOWS\help\iisHelp\common\401-1.htm
                  401,2,FILE,C:\WINDOWS\help\iisHelp\common\401-2.htm"

If I transform it, the new line is translated to hex(&#xA;) in the output: HttpErrors="400,*,FILE,C:\WINDOWS\help\iisHelp\common\400.htm&#xA;

But this works for you? IIS accepts these values anyway?
Well, if it does, great. That means I've just slipped up somewhere else in saving the changes back out to metabase.xml
0
How To Reduce Deployment Times With Pre-Baked AMIs

Even if we can't include all the files in the base image, we can sometimes include some of the larger files that we would otherwise have to download, and we can also sometimes remove the most time-consuming steps. This can help a lot with reducing deployment times.

 

Author Comment

by:Inward_Spiral
ID: 13445007
Okay, I just got it to work by getting rid of the "&#xA;" characters, recursively replacing them all with"&#11;".

<xsl:template name="replaceChar">
<xsl:param name="string" />
      <xsl:if test="contains($string, '&#xA;')">
        <xsl:value-of select="substring-before($string, '&#xA;')" /><xsl:text>&#11;</xsl:text>
        <xsl:call-template name="replaceChar">
              <xsl:with-param name="string"><xsl:value-of select="substring-after($string, '&#xA;')" /></xsl:with-param>
                 </xsl:call-template>
      </xsl:if>
      <xsl:if test="not(contains($string, '&#xA;'))"><xsl:value-of select="$string" /></xsl:if>
</xsl:template>

Seems like a lot of work to get the attribute values to work, but there you go.

0
 
LVL 26

Expert Comment

by:rdcpro
ID: 13445533
I don't get the &#xA; characters at all.  You're seeing an encoding problem, I think, related to how you're performing the transformation.  

What does your xsl:output tag say, and precisely how are you doing the transform?

Regards,
Mike Sharp
0
 

Author Comment

by:Inward_Spiral
ID: 13445666

My output tag is this:
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>

Nothing majorly different from what you posted earlier.

I think it might be what I'm using to process the XSLT with. When I use .NET for the XSL transformation, I get the &#xA; characters. When I use Xalan or Saxon with my XSLT debugger, I don't get the characters.

Any other theories on that one?

0
 

Author Comment

by:Inward_Spiral
ID: 13445692
What are you using to process the XSLT with? Maybe that's the question I should have asked.
0
 
LVL 26

Expert Comment

by:rdcpro
ID: 13446160
I was using XML Spy in my case.  The 0xA is illegal anyway--it should be x0A or more likely, x0D x0A, since it's really two characters; carriage return and linefeed. I think your problem is definitely in the encoding or somewhere in your transform, but you can probably cheat by removing the indent="yes" in your xsl:output.  This should prevent the processor from indenting anything at all--it will come out on a single line.  Then ASP.NET will "refresh" it when it feels like it, and it will be pretty printed again.  I don't see what schedule ASP.NET uses to refresh the thing, either, but it does.

Also make sure your .NET code isn't trying to do any indentation...

Regards,
Mike Sharp

0
 
LVL 26

Expert Comment

by:rdcpro
ID: 13446200
Actually, that may not help either.  I wonder if there's some whitespace normalization that's going on here...does your code explicitly set PreserveWhiteSpace?  

XmlDocument.PreserveWhitespace Property
    Gets or sets a value indicating whether to preserve white space.

    true to preserve white space; otherwise false. The default is false.

This property determines how white space is handled during the load and save process.

If PreserveWhitespace is true before Load or LoadXml is called, white space nodes are preserved; otherwise, if this property is false, significant white space is preserved, white space is not.

If PreserveWhitespace is true before Save is called, white space in the document is preserved in the output; otherwise, if this property is false, XmlDocument auto-indents the output.

This method is a Microsoft extension to the Document Object Model (DOM).
0
 

Author Comment

by:Inward_Spiral
ID: 13452565
This is getting interesting...maddening, but interesting. That "fix" I mentioned earlier only works if I'm using the XSLT Magic Profiler that I downloaded from Microsoft. Go figure.

When I actually run it through our .NET code to transform it, I get code like this within the attribute:
403,7,Forbidden,Client certificate required,0&#xD;&#xA;                  403,7,Forbidden,Client certificate required,0&#xD;&#xA;

The "&#xD;&#xA;" characters show up instead of a carriage return and a line feed.

This only occurs if I'm using the xsl:attribute function. If I render that value as shown below, I get those extra characters.
<xsl:attribute name="{name()}"><xsl:value-of select="$theAttributeValue"/></xsl:attribute>

However, If I map that value to a new node like below, then the line feeds and the carriage returns are mapped over correctly.
<textNode>403,7,Forbidden,Client certificate required,0                  
          403,7,Forbidden,Client certificate required,0</textNode>

I tried the PreserveWhitespace property, but that didn't change the output. How can I get the special characters like line feeds to be mapped over correctly without showing up as "&#xA;"?

You asked earlier why no one has tried this before...I think this is why!
0
 
LVL 26

Expert Comment

by:rdcpro
ID: 13453379
I think if they were &#x0D;&#x0A it would probably be fine.  &#xD;&#xA are not valid UTF-8 character entities. It looks like the result of the transform is somehow coming out as an 8 bit character encoding...

What happens if you try to open the XML output file in Internet Explorer?  Do you get an error (ie: "switch from current encoding is not allowed here" or some such)?

I'll try setting up a .NET transform myself, and see what happens.

Regards,
Mike Sharp
0
 

Author Comment

by:Inward_Spiral
ID: 13454490
Well, I think this might just work...even though there are still some invalid characters show up in the output, it can be loaded in IE, and I just added 2 application pools to IIS with the output file.

I did have to switch encoding though, I was sending output at UTF-16 by default.

0
 
LVL 26

Expert Comment

by:rdcpro
ID: 13454685
I hate to say this, but I can't get it to fail.  I take back what I said about the character entities...these aren't the hex codes themselves, but a character reference.  So &#xD;&#xA; is functionally equivalent to &#13;&#11; as far as a parser is concerned.  Once the character references are expanded, *then* they become 0D0A.

My Metabase.xml, once it's transformed, has the &#xD;&#xA; just like yours.  But my IIS doesn't seem to be bothered by it.  If I reboot, or if I load the IIS Services manager MMC, or if I simply wait a while, the metabase.xml gets rewritten (pretty printed), and since the character references are gone, I'm assuming the base class that handles this is using an xml text writer, which might not excape these characters. In an XSLT, within an attribute, it seems like it *always* wants to escape them.  Even if I add this to my transform:

<xsl:template match="@*">
      <xsl:attribute name="{local-name()}"><xsl:value-of select="." disable-output-escaping="yes"/></xsl:attribute>
</xsl:template>

I seem to remember somewhere that disable-output-escaping is overridden if you try to do it in an attribute.

Anyway, it works fine for me no matter what I do.  Here's my transform code (a console app):

using System;
using System.Text;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;

namespace BasicTransform
{
      /// <summary>
      /// Summary description for Class1.
      /// </summary>
      class MyTransform
      {
            /// <summary>
            /// The main entry point for the application.
            /// </summary>
            [STAThread]
            static void Main(string[] args)
            {
                  XmlDocument xml = new XmlDocument();
                  xml.PreserveWhitespace = true;
                  xml.Load(@"V:\WINDOWS\system32\inetsrv\Metabase.xml");

                  XPathNavigator nav = xml.CreateNavigator();
                  
                  Encoding enc = Encoding.UTF8;
                  XmlTextWriter writer = new XmlTextWriter(@"V:\WINDOWS\system32\inetsrv\Metabase.xml", enc);

                  // Create a resolver with default credentials.
                  XmlUrlResolver resolver = new XmlUrlResolver();
                  resolver.Credentials = System.Net.CredentialCache.DefaultCredentials;

                  XslTransform xsl = new XslTransform();
                  xsl.Load("identity.xslt");
                  xsl.Transform(nav,null,writer,resolver);

            }
      }
}

Regards,
Mike Sharp
0
 
LVL 26

Expert Comment

by:rdcpro
ID: 13454720
Ah that's good to hear.  I couldn't make it fail at all!

Mike
0
 

Author Comment

by:Inward_Spiral
ID: 13455195
Still one more hurdle in what I'm trying, though. I'm trying to automatically move my applications into different application pools.
I think my XPath is off, it shouldn't be as hard as I'm making it out to be.

Each application is set as an IIsConfigObject by default, and every value is set in several <Custom> nodes.
<IIsConfigObject Location="/../../myWebAppLocation">
...
     <Custom
          Name="AppPoolID"
          ID="0"
          Value="AppPool1"
          Type="STRING"
          UserType="UNKNOWN_UserType"
          Attributes="NO_ATTRIBUTES"
     />
...
</IIsConfigObject>

I added a case statement to your root template below, for a test to catch the IIsConfigObjects, but I must not be using the right XPath to select the Custom nodes. I stepped through it and can make it to the "name() = 'IIsConfigObject'" case, but it doesn't get to the Custom node.

What's a simple way to access the Custom nodes?
I'm betting I'm just making it harder than it has to be...which has usually been the case in this post. :)

<xsl:template match="/ | node()">
 <xsl:choose>
  <xsl:when test="name() = 'IIsConfigObject' and contains(@Local,'myWebAppLocation')">
      <xsl:variable name="AppLocation" select="@Location"/>
       <xsl:variable name="AppPoolIDValue">
           <xsl:for-each select="Custom">
             <xsl:if test="@Name='AppPoolID'"><xsl:value-of select="@Value"/></xsl:if>
           </xsl:for-each>
       </xsl:variable>
       <xsl:element name="VirtualWebDir">
          <xsl:attribute name="Location"><xsl:value-of select="$AppLocation"/></xsl:attribute>
          <xsl:attribute name="AppPoolID"><xsl:value-of select="$AppPoolIDValue"/></xsl:attribute>
       </xsl:element>
  </xsl:when>
  <xsl:otherwise>
     <xsl:copy>
          <xsl:apply-templates select="@*"/>
          <xsl:apply-templates select="node()"/>
     </xsl:copy>
  </xsl:otherwise>
 </xsl:choose>
</xsl:template>
0
 
LVL 26

Expert Comment

by:rdcpro
ID: 13462240
For Identity transformations, I find it better to put the modifications in separate templates.  So rather than modify the general template for nodes, add a new template that explicitly matches your condition:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  xmlns:out="urn:microsoft-catalog:XML_Metabase_V54_0">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>

<xsl:template match="/ | node()">
      <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:apply-templates select="node()"/>
      </xsl:copy>
</xsl:template>

<xsl:template match="@*">
      <xsl:copy>
      </xsl:copy>
</xsl:template>

<!-- Templates to handle changes go here -->

<xsl:template match="@HttpCustomHeaders">
      <xsl:attribute name="HttpCustomHeaders">X-Powered-By: MikeSharp</xsl:attribute>
</xsl:template>


<xsl:template match="IIsConfigObject[contains(@Location, 'myWebApp')]">
      <!-- Creates a new VirtualWebDir element based on the current IISConfigObject -->
      <xsl:element name="VirtualWebDir">
            <xsl:attribute name="Location"><xsl:value-of select="@Location"/></xsl:attribute>
            <xsl:attribute name="AppPoolID"><xsl:value-of select="Custom[@Name='AppPoolID']/@Value"/></xsl:attribute>
      </xsl:element>
      <!-- copies the current IISConfigObject node and children -->
      <xsl:copy>
             <xsl:apply-templates select="@*"/>
             <xsl:apply-templates select="node()"/>
      </xsl:copy>
</xsl:template>

</xsl:template>


</xsl:stylesheet>



Again, watch out for namespaces!

Regards,
Mike Sharp
0
 

Author Comment

by:Inward_Spiral
ID: 13462423
You weren't kidding about those namespaces.

My output generates "xmlns" for the new nodes I'm creating.

Thankfully, MS strips those out of the file when IIS is restarted, and works anyway. It does leave this message in the log though:
"The property (xmlns) is not valid for the class it has been associated with.  This property will be ignored.  Incorrect XML:xmlns"

0
 
LVL 26

Expert Comment

by:rdcpro
ID: 13462757
You can get rid of these if you set up the namespaces in the XSLT correctly.  For example, if my XML root element looks like:

<configuration xmlns="urn:microsoft-catalog:null-placeholder">

And my XSLT root element looks like:

<xsl:stylesheet version="1.0"
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  
      xmlns:out="urn:microsoft-catalog:null-placeholder"  
      xmlns="urn:microsoft-catalog:null-placeholder"
      exclude-result-prefixes="out">


and my template uses the out: prefix in the element XPath expression:

<xsl:template match="out:IIsConfigObject[contains(@Location, 'tsweb')]">
      <!-- Creates a new VirtualWebDir element based on the current IISConfigObject -->
      <xsl:element name="VirtualWebDir">
            <xsl:attribute name="Location">
                <xsl:value-of select="@Location"/>
            </xsl:attribute>
            <xsl:attribute name="AppPoolID">
                <xsl:value-of select="out:Custom[@Name='AuthFlags']/@Value"/>
            </xsl:attribute>
      </xsl:element>
      <!-- copies the current IISConfigObject node and children -->
      <xsl:copy>
             <xsl:apply-templates select="@*"/>
             <xsl:apply-templates select="node()"/>
      </xsl:copy>
</xsl:template>

then I get the correct output nodes with no namespaces:

<VirtualWebDir Location="/LM/W3SVC/1/ROOT/tsweb" AppPoolID="AuthNTLM" />
<IIsConfigObject Location="/LM/W3SVC/1/ROOT/tsweb">
    <Custom
                    Name="AccessFlags"
                    ID="6016"
                    Value="AccessRead | AccessScript"
                    Type="DWORD"
                    UserType="IIS_MD_UT_FILE"
                    Attributes="INHERIT">
    </Custom>
    <Custom
                    Name="AuthFlags"
                    ID="6000"
                    Value="AuthNTLM"
                    Type="DWORD"
                    UserType="IIS_MD_UT_FILE"
                    Attributes="INHERIT">
    </Custom>
    <Custom
                    Name="DefaultDoc"
                    ID="6006"
                    Value="Default.htm,Default.asp"
                    Type="STRING"
                    UserType="IIS_MD_UT_FILE"
                    Attributes="NO_ATTRIBUTES">
    </Custom>
[...]

Please note, the data is bogus--I'm not precisely sure what your desired output was, but in this case I created a new VirtualWebDir element ahead of the IIsConfigObject that matches the template.  Then I created two attributes with some data from the IIsConfigObject element.  

Also note that I'm specifying BOTH a prefixed namespace and a default namespace in the XSLT root element.  One governs the namespace used for select statements (the one with the out:prefix), and the other is used for the namespace of the output.  In retrospect, I named that prefix a little misleadingly...I usually use something like user:  or temp: for these.

Regards,
Mike Sharp
0
 
LVL 26

Expert Comment

by:rdcpro
ID: 13462805
Oh, one other suggestion.  You can use xsl parameters to specify, before the transformation, what values you want to test for and what values you want to substitute.  

For example, instead of this:

<xsl:template match="out:IIsConfigObject[contains(@Location, 'tsweb')]">

you can use:

<xsl:param name="pConfigObj" >tsweb</xsl:param>

[...]

<xsl:template match="out:IIsConfigObject[contains(@Location, $pConfigObj)]">

These parameters can be set externally, before the transform.  You could even load a different XML file that has the build information in it, and either include it into the XSLT using the document() function, or parse it and set the parameters for the transform.  Lotsa possibilities.

Regards,
Mike Sharp
0
 

Author Comment

by:Inward_Spiral
ID: 13463371
Lotsa possibilities...there's the kicker.
I'll pick one and go with it, and see if I can make it without any "xmlns".

Other than that, it's looking pretty good.

One lesson learned: Do NOT use XSLT Majic to debug when working on the IIS metabase.
For some reason, the whitespace characters came out corrupted somehow, and I had to replace with &#11; to get it to work.

If I'm stepping through my .NET code, I don't have that issue.
0
 

Author Comment

by:Inward_Spiral
ID: 13507889
Mike, thanks for all your help with this.
I've still got some kinks to iron out, but that's just going to require more testing.

Sorry I didn't award you the points sooner!
0
 
LVL 26

Expert Comment

by:rdcpro
ID: 13508675
No worries, glad to help.

Regards,
Mike Sharp
0

Featured Post

Optimize your web performance

What's in the eBook?
- Full list of reasons for poor performance
- Ultimate measures to speed things up
- Primary web monitoring types
- KPIs you should be monitoring in order to increase your ROI

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

I found this questions asking how to do this in many different forums, so I will describe here how to implement a solution using PHP and AJAX. The logical flow for the problem should be: Write an event handler for the first drop down box to get …
Create a Windows 10 custom Image with custom task bar and custom start menu using XML for deployment.
The viewer will learn how to dynamically set the form action using jQuery.
The viewer will learn how to create a basic form using some HTML5 and PHP for later processing. Set up your basic HTML file. Open your form tag and set the method and action attributes.: (CODE) Set up your first few inputs one for the name and …
Suggested Courses

771 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question