Adding Document Icon Definitions in Sharepoint 2007

A large part of my job requires me to manage and support a large MOSS 2007 environment. While I do have a lot of other people from other teams who help support it, the team I belong to is quite small. With a small team, anything that I can do to save myself some time is beneficial. That is a major reason behind my push into Powershell. There are so many things that you can do with Powershell since it is based on the .NET Framework.

When the environment was first being set up, one of the things needed was to add a document icon definition for PDF files. Sharepoint 2007 does not have the icon definition out-of-the-box. Sharepoint does, however, provide a file—the DOCICON.XML file—that will allow you to add additional icon file definitions to your system. Unfortunately, you have to update this file on every web front-end server in your farm. In order to update this, you have a couple of options:

  • Edit each individual file (I’m not real fond of this option.)
  • Edit one file and copy it out to the same location on your other front-end servers (A little better than the first option, but still not feelin’ it.)
  • Leverage Powershell to manage these changes across your farm (Now, we’re talkin’!)

For the purposes of this article, I am going to assume that you have all of your servers in relatively close proximity. If you have the luxury of a geographically distributed farm around the world, this may help, but you may want to look into setting up some jobs to make this happen in parallel across many servers at one time. In this case, I am going to present this as a small two-server farm that is in the same datacenter.

Powershell has built-in support for XML documents. As such, it makes being able to navigate around an XML pretty simple once you understand a few items. I will touch on a couple of XPath syntax here, but for a complete tutorial, you can check out the W3Schools.com XPath Tutorial. When I started to research the scripts here, I used this resource to understand how to find things in an XML file.

The script I am going to show will allow you to specify a list of servers, the extension or the “ProgID” of the document, the icon image file, the edit text for the icon, and the OpenControl definition. For a more thorough explanation of the DOCICON.XML file, please refer to the MSDN Documentation on the subject.

Now to some code…the first things we need to do is to load the Microsoft.Sharepoint.Utilities namespace into memory.

 [void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Utilities")

This will allow us to attach to Sharepoint and get the location of the installation. Since I can’t know (and really don’t want to look up) where an installation is on a system, this helps me to find it. This does assume that your web front-end servers are installed identically. To get the path, I can use the GetGenericSetupPath method of the SPUtility class to create the filename. Since I anticipate having to connect to other servers on the same path, I am going to also convert this path to a UNC path, even to connect to the local machine.

 $xmlFilename = [Microsoft.SharePoint.Utilities.SPUtility]::GetGenericSetupPath("TEMPLATE\XML\DOCICON.XML").Replace(":","$").Insert(0,"\\$ComputerName\") 

In this example, I tack on the relative path (“TEMPLATE\XML\DOCICON.XML”) to the returned Setup path, then replace the “:” with a “$” (“C:” -> “C$”), and then finally tack the “\\<computername>\” in front to complete the path. Now that I know where to find the file, I can read the file as an XML document into memory.

 $DocIconFile = [xml](get-content $xmlFilename) 

Now that the schema for the XML file is in memory, I can build the element for the new icon definition. Using the CreateElement method of the XMLDocument class, I will add the attributes that will eventually be saved back to the XML file.

 $newIcon = $DocIconFile.CreateElement("Mapping")
$newIcon.SetAttribute("Key","$extension")
$newIcon.SetAttribute("Value","$iconFile")
if ($openControl){
   $newIcon.SetAttribute("OpenControl","$openControl")
}
if ($editText){
    $newIcon.SetAttribute("EditText","$editText")
} 

I put a couple of tests in here to keep the resulting XML file a bit cleaner. Since the OpenControl and the EditText attributes are not required, I am testing to see if they were entered. If not, they won’t show up on the final element to be added to the document. Now that I have the new element created, I can look for a suitable location to add it. One of the first things I do in the script is to run a check for the existing definition. If it already exists, then a new one should not be added. I haven’t tested to see what happens if there are multiple definitions in the file, but I can’t imagine it would be great. Even if it didn’t cause a major problem, it would make troubleshooting a nightmare after a while. If the definition is found, I just update the definition and save the file, but only if the “force” switch is used on the script.

 $existingMapping = $DocIconFile.DocIcons.ByExtension.Mapping | Where-Object {$_.Key -eq $extension}
       if ($existingMapping -ne $null) {
            Write-Debug -Message "The mapping already exists."
            If ($force) {
                Write-Debug -Message "You are forcing me to update the existing mapping."
                ($DocIconFile.DocIcons.ByExtension.Mapping | Where {$_.Key -eq $extension}).Value = $iconFile
            }
            else
            {
                Write-Debug -Message "Not forced to update the mapping...Exiting script."
                Write-Host "Icon mapping already exists for this extension."
                Exit            
            }
  } 

If the mapping does not exist then processing drops to the “else” block of the “if” statement.

 else
{
    Write-Debug -Message "The mapping does not already exist."
    Write-Debug -Message "Looking at each extension mapping..."
    Foreach ($mapping in $DocIconFile.DocIcons.ByExtension.Mapping){
         if ($mapping.Key -gt $extension){
            Write-Debug -Message "We found the target, now back up and insert the new entry and exit the loop."
  [System.Xml.XmlElement]$mapping.ParentNode.InsertBefore($newIcon, [System.Xml.XmlElement]$mapping)
             Break
         }
    }        
}

This block of code provided a bit of a challenge for me at first. When first creating this block, I was doing my debugging in the Powershell console window. The console window is a little more forgiving sometimes because of the context of the commands. My first iteration of code looked something like this:

$DocIconFile.DocIcons.ByExtension.Mapping | Foreach-Object {
    if ($_.Key –gt "pdf") {
        [System.Xml.XmlElement]$_.ParentNode.InsertBefore($newIcon, [System.Xml.XmlElement]$_)
        break
        }
    }

This configuration works in a console window because when the script “breaks,” it goes back to the prompt. Unfortunately, the same behavior is not good inside the script that is being created here. There is a HUGE difference between

$DocIconFile.DocIcons.ByExtension.Mapping | Foreach-object {some block of Powershell code}

And

$mappings = $DocIconFile.DocIcons.ByExtension.Mapping
foreach ($mapping in $mappings){ Do something with $mapping until something is true; break}

The difference is that the first example is a Foreach-object cmdlet and is NOT a looping construct. This example runs through each item that is passed through the pipeline, one at a time. When the “break” is reached, it terminates the script, and not a loop, since it is not a loop construct.

The second example, however, IS a looping construct…so when the “break” is encountered, the loop is exited and not the entire script. Part of the way I found this was by using the Write-Debug lines that you see in the script. You can access these lines by adding “-debug” as a parameter to the script when it runs. It will show you those lines so you can see which direction the conditional statements are going.

Finally, when the location is found for the definition to be placed, we take that location, and insert the new definition before the definition found so that they are still in alphabetical order in the XML file.

In the full source code list, you will also see a section that looks very similar to the above code. The only difference is that I check for the ProgID of the definition, if that is what is supplied to the script. The same exact procedure is used whether the script uses the “extension” or the “progid” element.

Below you will find the full source code for the script. Please leave comments with questions or suggestions you have about the script. The next installment (which will be a lot shorter) will detail how to use the same type of concept and delete an entry from this file.

 [CmdletBinding()]
PARAM(
    [parameter(Mandatory=$true,ValueFromPipeline=$true)]
    [string[]] $computername=$env:computername,
    [string] $extension,
    [string] $progId,
    [parameter(Mandatory=$true)]
    [string] $iconfile,
    [string] $editText="",
    [string] $OpenControl="",
    [switch]$Force
    )
BEGIN{
    Write-Debug -Message "Begin block."
}
PROCESS{
    Write-Debug -Message "Checking if both extension and progid ARE NOT specified."
    If ((!$extension) -AND (!$progId)) {
        Write-Host "You must provide either an extension or a Program ID." -fore yellow
        Write-Debug -Message "Neither parameter was provided...exiting."
        Exit
    }
    Write-Debug -Message "Checking if both extension and progid ARE specified"
    If (($progId) -AND ($extension)){
        Write-Host "You cannot use an extension and a Program ID at the same time." -fore yellow
        Write-Debug -Message "Both parameters were specified...exiting."
        Exit
    }
    Write-Debug -Message "Loading Sharepoint Namespaces..."
    [void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Utilities")
    Write-Debug -Message "Getting the path to the DOCICON.XML file."
    $xmlFilename = [Microsoft.SharePoint.Utilities.SPUtility]::GetGenericSetupPath("TEMPLATE\XML\DOCICON.XML").Replace(":","$").Insert(0,"\\$ComputerName\")
    Write-Debug -Message "Reading the XML file into an XML document in memory."
  $DocIconFile = [xml](get-content $xmlFilename)
  Write-Debug -Message "Creating the NewIcon object."
    $newIcon = $DocIconFile.CreateElement("Mapping")
    $newIcon.SetAttribute("Key","$extension")
  $newIcon.SetAttribute("Value","$iconFile")
    if ($openControl){
        $newIcon.SetAttribute("OpenControl","$openControl")
    }
    if ($editText){
        $newIcon.SetAttribute("EditText","$editText")
    }
    Write-Debug -Message "Checking to see if we are using the 'extension' mapping."
    if ($extension){
        Write-Debug -Message "We ARE using the 'extension' mapping."
        Write-Debug -Message "Checking to see if the mapping already exists."
        $existingMapping = $DocIconFile.DocIcons.ByExtension.Mapping | Where-Object {$_.Key -eq $extension}
        if ($existingMapping -ne $null) {
            Write-Debug -Message "The mapping already exists."
            If ($force) {
                Write-Debug -Message "You are forcing me to update the existing mapping."
                ($DocIconFile.DocIcons.ByExtension.Mapping | Where {$_.Key -eq $extension}).Value = $iconFile
            }
            else
            {
                Write-Debug -Message "Not forced to update the mapping...Exiting script."br/>                Write-Host "Icon mapping already exists for this extension."
                Exit
            }
        }
        else
        {
            Write-Debug -Message "The mapping does not already exist."
            Write-Debug -Message "Looking at each extension mapping..."
            Foreach ($mapping in $DocIconFile.DocIcons.ByExtension.Mapping){
                if ($mapping.Key -gt $extension){
                    Write-Debug -Message "We found the target, now back up and insert the new entry and exit the loop."
                    [System.Xml.XmlElement]$mapping.ParentNode.InsertBefore ($newIcon, [System.Xml.XmlElement]$mapping)
                    Break
                }
            }
}
    Write-Debug -Message "Checking to see if we are using the 'progid' mapping."
    if ($progId) {
        Write-Debug -Message "We ARE using the 'progid' mapping."
        Write-Debug -Message "Checking to see if the mapping already exists."
        $existingMapping = $DocIconFile.DocIcons.ByProgID.Mapping | Where-Object{$_.Key -eq $progId}
        if ($existingMapping -ne $null) {
            Write-Debug -Message "The mapping already exists."
            If ($force) {
                Write-Debug -Message "You are forcing me to update the existing mapping."
                (DocOconFile.DocIcons.ByProgID.Mapping | Where {$_.Key -eq $progId}).Value = $iconFile
            }
            else
            {
                Write-Debug -Message "Not forced to update the mapping...Exiting script."
                Write-Host "Icon mapping already exists for this program ID."
                Exit
            }
        }
        else
        {
            Write-Debug -Message "The mapping does not already exist."
            Write-Debug -Message "Looking at each progid mapping..."
            Foreach ($mapping in $DocIconFile.DocIcons.ByProgID.Mapping){
                if ($mapping.Key -gt $extension){
                    Write-Debug -Message "We found the target, now back up and insert the new entry and exit the loop."
                    [System.Xml.XmlElement]$mapping.ParentNode.InsertBefore ($newIcon, [System.Xml.XmlElement]$mapping)
                    Break
                }
            }
        }
    }
    Write-Debug -Message "Saving the file."
    $DocIconFile.Save($xmlFilename)
}
END{
    Write-Debug -Message "Ending block."
}
Advertisements
This entry was posted in Sharepoint 2007, XML and tagged , , , , , , . Bookmark the permalink.