Audit your Content Type Hierarchy


Shortly after you deploy a SharePoint site you probably start to wonder how your carefully designed content types are actually being used. And abused. More importantly I want to know if any of my feature based content types have been disconnected (unghosted) from the XML source file.

To that end I looked into extracting and visualizing the content type hierarchy to get the overview – and I found that it was already built by some nice chaps at codeplex (can be found here)! The community works like never before ;-)

I changed it in a number of ways (in my mind improved it):

  • The original only showed the content types declared at the site collection root, now I use a completely different way of finding inherited content types through the SPContentTypeUsage classes, to be able to show derived content types in sub sites as well
  • Using the SPContentTypeUsage will also give you derived list content types, so they can be displayed as well. There might be a large number of lists, so you can switch the list content type display on and off. Default is off.

    Caveat: To figure out whether a given (list) content type is a direct descendent of another I have to open the actual list content type and check. Some lists are special (like _catalogs) and will be rendered slightly off in the hierarchy (hierarchy “missing” but the list content type location will be there)

  • Added my very own detection of the ghosting/unghosting status of any given content type. There are three states: ghosted (connected to xml source), unghosted (disconnected from xml source) and “DB only” (created through the UI or OM, not from a feature)

At the end of the day pretty much all of the code has been changed, while keeping the layout. I’m still grateful for the very nice first step provided and I will post this code back to the codeplex site. Hopefully the guys there will accept the modified code.
[Updated April 14: Patch posted now]

Detecting Ghosted/Unghosted State

I did a lot of digging to figure out how to get at the ghosting status of a content type. You can have a look in the wss content database (table “ContentTypes”) to see if your content types are disconnected or not (column “definition” is null – see my other blog on this). There’s even a dedicated stored procedure that will tell you the ghosting state of a given content type (named “proc_IsContentTypeGhosted”).

I really did not want to directly call stored procedures on the database, so I had a good long hard look at the properties available through the SPContentType class (and many others through reflector) and came up with the following conditions for detecting the state:

  • Is it a feature based content type, by checking “FeatureID != null”, if not mark it as “DB only”. Note that this is actually a private field, so I have to go the long way around and fetch it through reflection.
  • If it is feature based on the version > 0 then it has been unghosted. The version number seem to work well for this. It will always be 0 for ghosted content types and be incremented by one every time somebody modifies (and thereby unghost it). According to the xml schema you can specify this field in your content type definition, but it will (fortunately) not make it to the database so the detection seems sound.

    If you make new versions of the xml file, the server won’t really notice (see post here).

Disclaimer: This algorithm works well for me, seems reasonable sensible, but there might be an edge case that I’m not aware of.

The Result

Here is what it looks like:

conenttypehierarchy.png

The page is rather long – you are seeing about half of it. It’s also slow, but that’s hardly a performance issue you should care to fix ;-)

The Code

You need to create a feature to deploy the page, the feature.xml I use:

<Feature xmlns="http://schemas.microsoft.com/sharepoint/" Id="2D9921AE-D263-4e2b-B4F7-ABDD6223C8B0"
   Scope="Site"
   Title="My Administration Utilities (Site collection)"
   Description="My adminsitration tools at the site collection level">

   <ElementManifests>
      <ElementManifest Location="ContentTypeHierarchy.xml" />
   </ElementManifests>
</Feature>

The referenced ContentTypeHierarchy.xml file (will create a link in the Gallery section of site settings):

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction Id="ContentTypeHierarchy"
   GroupId="Galleries"
   Location="Microsoft.SharePoint.SiteSettings"
   Sequence="0"
   Title="Site Content Types Hierarchy"
   Description="Displays a hierarchy of content types.">

   <UrlAction Url="_layouts/ContentTypeHierarchy.aspx"/>
</CustomAction>
</Elements>

And finally the magic page, ContentTypeHierarchy.aspx (deployed to the TEMPLATE\Layouts folder) (you can download it here, you’ll have to rename it back to aspx):

<%@ Page Language="C#" MasterPageFile="~/_layouts/application.master" %>

<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral,PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Import Namespace="Microsoft.SharePoint.WebControls" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Reflection" %>
<%@ Register TagPrefix="wssuc" TagName="ToolBar" Src="~/_controltemplates/ToolBar.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="ToolBarButton" Src="~/_controltemplates/ToolBarButton.ascx" %>
<asp:Content ID="Content2" runat="server" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea">
	Content Types Hierarchy
</asp:Content>
<asp:Content ID="Content3" runat="server" ContentPlaceHolderID="PlaceHolderPageDescription">
	This page shows all Site Content Types and their hierarchical relationships
</asp:Content>
<asp:Content ID="Content4" runat="server" ContentPlaceHolderID="PlaceHolderMain">

	<table border="0" width="100%" cellspacing="0" cellpadding="0">
		<tr>
			<td id="mngfieldToobar">
				<wssuc:ToolBar id="onetidMngFieldTB" runat="server">
					<template_buttons>
					  <wssuc:ToolBarButton runat="server" Text="<%$Resources:wss,multipages_createbutton_text%>" id="idAddField" ToolTip="<%$Resources:wss,mngctype_create_alt%>" NavigateUrl="ctypenew.aspx" ImageUrl="/_layouts/images/newitem.gif" AccessKey="C" />

						<table cellpadding="1" cellspacing="0" border="0">
							<tr>
								<td class="ms-toolbar" nowrap style="padding:0px">
									<asp:CheckBox runat="server" CssClass="ms-toolbar" Text="Show lists" id="cbList" AutoPostBack="true" />
								</td>
							</tr>
						</table>                

						<table cellpadding="1" cellspacing="0" border="0">
							<tr>
								<td class="ms-toolbar" nowrap style="padding: 0px">
									<asp:CheckBox runat="server" CssClass="ms-toolbar"  Text="Show descriptions" id="cbDesc" AutoPostBack="true" Checked="true" />
								</td>
							</tr>
						</table>
					</template_buttons>
				</wssuc:ToolBar>
			</td>
		</tr>
	</table>
	<asp:PlaceHolder ID="phHierarchy" runat="server" />
</asp:Content>

<script runat="server">
    Hashtable contentTypesShown = new Hashtable();

    protected override void OnLoad(EventArgs e)
    {
		base.OnLoad(e);

		try {
			EnsureChildControls();
			cbDesc.CheckedChanged += new EventHandler(CheckedChanged);
			cbList.CheckedChanged += new EventHandler(CheckedChanged);
			BuildHierarchy();
		}
		catch (Exception x) {
			OutputHtml(x.Message);
		}
	}

	void CheckedChanged(object sender, EventArgs e) {
		BuildHierarchy();
	}

	public void BuildHierarchy() {
		EnsureChildControls();
		phHierarchy.Controls.Clear();
        contentTypesShown.Clear();
		SPSite site = SPControl.GetContextSite(Context);
		SPContentTypeCollection types = site.RootWeb.ContentTypes;

		SPContentTypeId id = types[0].Id;
		OutputHtml("<table style='font-size:10pt' border='0'" + " cellpadding='2' width='100%'><tr><td><ol>");
		ShowContentType(types[0]);
		OutputHtml("</ol></td></tr></table>");
	}

	public void OutputHtml(string html) {
		Literal l = new Literal();
		l.Text = html;
		phHierarchy.Controls.Add(l);
	}

	public void ShowContentType(SPContentType currenttype) {
		try {
			OutputHtml("<li><a class='ms-topnav'" +
				" href=\"" + currenttype.Scope.TrimEnd('/') +
				"/_layouts/ManageContentType.aspx?ctype=" +
				currenttype.Id.ToString() + "\">" + currenttype.Name +
				"</a>" + ExtraInfo(currenttype) +
				(cbDesc.Checked ? "<span class='ms-webpartpagedescription'>" + currenttype.Description + "</span>" : "") +
				"</li>");

			SPContentTypeId id = currenttype.Id;

			OutputHtml("<ol>");

			System.Collections.Generic.IList<SPContentTypeUsage> usages = SPContentTypeUsage.GetUsages(currenttype);

			int listCount = 0;
            foreach (SPContentTypeUsage usage in usages)
            {
                if (!contentTypesShown.ContainsKey(usage.Url + "|||" + usage.Id))
                {
                    contentTypesShown.Add(usage.Url + "|||" + usage.Id, true);

                    if (!usage.IsUrlToList)
                    {
                        using (SPWeb subSite = SPControl.GetContextSite(Context).OpenWeb(usage.Url))
                        {
                            SPContentType subType = subSite.ContentTypes[usage.Id];
                            if (subType.Parent.Id == id && subType.Parent.Id != subType.Id)
                            {
                                ShowContentType(subType);
                            }
                        }
                    }
                    else if (usage.IsUrlToList && cbList.Checked)
                    {
                        //1. Find web + list
                        //2. Open content type
                        listCount++;

                        Match m = Regex.Match(usage.Url, "^(?<web>.*?/)(Lists/)?(?<list>[^/]+)$");
                        if (m.Success)
                        {

                            try
                            {

                                using (SPWeb subSite = SPControl.GetContextSite(Context).OpenWeb(m.Groups["web"].Value))
                                {
                                    SPContentType subType = subSite.Lists[m.Groups["list"].Value].ContentTypes[usage.Id];
                                    if (subType.Parent.Id == id && subType.Parent.Id != subType.Id)
                                    {
                                        OutputHtml("<li>(List) <a class='ms-topnav'" +
                                            " href=\"" + m.Groups["web"] + "_layouts/ManageContentType.aspx?ctype=" +
                                            usage.Id.ToString() + "&list=" + subType.ParentList.ID.ToString() + "\">" + subType.Name + "</a> " + usage.Url + "</li>");
                                    }
                                }
                            }
                            catch (Exception e)
                            {
                                //exceptions occur for some of the "special" list, e.g. catalogs, then just list the list ;-)
                                OutputHtml("<li>(List) " + usage.Url + "</li>");
                            }
                        }
                    }
                }
            }

			OutputHtml("</ol>");
		}
		catch (Exception x) {
			OutputHtml("<b>Error:" + x.ToString() + "</b>");
		}

	}

	string ExtraInfo(SPContentType type) {
		string info = "";

		//Ghosted?
		// if the content type came from a feature and version > 0 then it's ghosted (as
		// far as I've been able to determine).
		// Version will not be greater than 0 for non-ghosted content types.
		// Alternative is to query DB directly (proc_IsContentTypeGhosted).

		//Need to find the featureId (if any) through reflection, since it's not a
		// public property)
		System.Reflection.PropertyInfo featureIdProp = type.GetType().GetProperty("FeatureId", BindingFlags.NonPublic | BindingFlags.Instance, null, typeof(Guid), Type.EmptyTypes, null);
		object featureId = featureIdProp.GetValue(type, null);

		if (type.Version > 0 && featureId != null && ((Guid)featureId) != Guid.Empty) {
			info += "Storage: <b>UNghosted (DB)</b>|";
		}
		else if (featureId != null && ((Guid)featureId) != Guid.Empty) {
			info += "Storage: Ghosted (Disk)|";
		}
		else {
			info += "Storage: DB only|";
		}

		if (type.ReadOnly) {
			info += "ReadOnly|";
		}
		if (type.Sealed) {
			info += "Sealed|";
		}
		info += "Scope:\"" + type.Scope + "\"|";

		info = info.TrimEnd('|').Replace("|", ", ");
		if (info.Length > 0) {
			info = " <i>(" + info + ")</i> ";
		}
		return info;
	}
</script>

Remember to reference the files in your solution manifest file and you should be good to go :-)

Summary

It works and I’m very happy with it. It is an invaluable tool to debug all sorts of content type related problems. Big thanks to the guys at codeplex for letting me hit the ground running on this.

What’s not in it? Site columns. Didn’t find a good way for it and the need seems limited.

Finally understand that this is not really an audit in the normal SharePoint sense, so you should also consider to enable auditing on content type changes as well. Even when you have locked down security properly accidents do happen (only for your fellow admins of course).

Propagate Site Content Types to List Content Types


[Aka: Make Content Type Inheritance Work for XML Based Content types]

[Note:Updated Nov 6]

If you are working with XML based content types, you’ll sooner or later fall into a trap well hidden by Microsoft. Simply put Content Type inheritance don’t work for XML based content types. SharePoint does not check what fields you’ve added/deleted/changed since you last deployed your content type feature, so you don’t have the “luxury” (it bloody well should work!) of choosing whether or not to propagate changes down to inherited content types as in the web interface or object model.

I found MS mention it briefly here.

The Role of List Content Types

I’ll take a small detour here to explain the concept of list content types. Whenever you assign a content type to a list (e.g. the pages document library for publishing webs) a new list content type is created which inherits from the site content type that you are assigning to the list and with the same name.

Actually the inheritance copies the site content type definition to the list content type in its entirety and assigns an “inherited” content type id to the new list content type.

If you later modify the site content type, through the web or object model, you have the option of propagating those changes to inherited content types, which in particular includes all the list content types on the site.

Don’t Disconnect the Content Type

Another small detour before I’ll present what to do about this mess. It should be stressed that any modification of the site content type through the web or object model, will disconnect the site content type from the underlying xml metadata file. If it happens you my reestablish the link as described here. This is very bad news. It not only means that you should be careful about what you do to your content types, but it also means that there is no simple way to propagate the changes code wise – we can’t just update the site content type (re-add fields) and have the changes propagate through the object model. :-(

The Code to Propagate

Finally I’m ready to present the code I used to propagate changes to my content types. :-)

The procedure is:

  1. Locate the site content type in question (run it multiple times if needed)
  2. Start at the root web site
    1. Go through all lists on the web
    2. If the list is associated with the site content type (actually the inherited list content type)
      1. Compare every field on the site content type with the list content type
      2. Add, remove or change the field in question on the list content type
    3. Go recursively through every subweb and continue from step a

I created the following code as a new extension to stsadm, called like this:

stsadm -o cbPropagateContentType -url <site collection url> -contenttype <contenttype name> [-verbose] [-removefields] [-updatefields]

For instance:

stsadm -o cbPropagateContentType -url http://localhost -contenttype “My Article Page” –verbose

The “removefields” switch specifies whether or not fields found in the list content type that are not in the site content type should be removed or not. Default is not. New fields in the site content type will always be added to the list content types.

Note: I’ve not tried to create an “update” field. I’ve not had a use for that yet and it will also require considerably more testing to ensure that it works correctly. You really don’t want to break your list content types in case of errors…

Updated (Nov 6): An update option has now been added you should consider that option as beta.

Note 2: If the job stops prematurely no harm is done. Restart it and it will continue from where it stopped – it will examine and skip the webs/lists that it already has processed.

Finally something tangible:

Save the following as “stsadmcommands.CBPropagateContentType.xml” and save it into “\Config” in the root of the SharePoint install folder (remember to update the assembly reference to whatever you compile the code into):

<?xml version="1.0" encoding="utf-8" ?>
<commands>
  <command name="cbpropagatecontenttype"
          class="Carlsberg.SharePoint.Administration.STSAdm.CBPropagateContentType,
          Carlsberg.SharePoint.Administration, Version=1.0.0.0, Culture=neutral,  
          PublicKeyToken=55c69d084ac6678f"/>
</commands>

And finally the code you need to compile to an assembly that the xml file should specify:

Updated (Nov 6): Code has been updated a bit. Some mistakes with display name/internal name have been fixed and the update option has been added. I’m not yet satisfied with the testing of the update method so consider it to be beta.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.StsAdmin;

namespace Carlsberg.SharePoint.Administration.STSAdm
{
    /// 
    /// A custom STSAdm command for propagating site content types to lists
    /// content types.
    /// 
    /// The code is provided as is, I don't take any responsibilty for 
    /// any errors or data loss you might encounter.
    /// 
    /// Use freely with two conditions:
    /// 1. Keep my name in there
    /// 2. Report any bugs back to http://soerennielsen.wordpress.com
    /// 
    /// Enjoy
    /// Søren L. Nielsen
    /// 
    /// 
    class CBPropagateContentType : ISPStsadmCommand
    {
        #region Input parameters
        private string providedUrl;
        private string contentTypeName;
        private bool removeFields = false;
        private bool verbose = false;
        private bool updateFields = false;

        private bool UpdateFields
        {
            get { return updateFields; }
            set { updateFields = value; }
        }

        private bool Verbose {
            get { return verbose; }
            set { verbose = value; }
        }

        private bool RemoveFields {
            get { return removeFields; }
            set { removeFields = value; }
        }

        private string ContentTypeName {
            get { return contentTypeName; }
            set { contentTypeName = value; }
        }

        private string ProvidedUrl {
            get { return providedUrl; }
            set { providedUrl = value; }
        }
        #endregion

        /// 
        /// Runs the specified command. Called by STSADM.
        /// 
        /// The command.
        /// The key values.
        /// The output.
        /// 
        public int Run(string command, StringDictionary keyValues, 
                       out string output) {
            //Parse input                        
            // make sure all settings are valid
            if (!GetSettings(keyValues)) {
                Console.Out.WriteLine(GetHelpMessage(string.Empty));
                output = "Required parameters not supplied or invalid.";
            }

            SPSite siteCollection = null;
            SPWeb rootWeb = null;

            try {
                // get the site collection specified
                siteCollection = new SPSite(ProvidedUrl);
                rootWeb = siteCollection.RootWeb;

                //Get the source site content type
                SPContentType sourceCT = 
                             rootWeb.AvailableContentTypes[ContentTypeName];
                if (sourceCT == null) {
                    throw new ArgumentException("Unable to find " 
                        + "contenttype named \"" + ContentTypeName + "\"");
                }

                // process the root website
                ProcessWeb(rootWeb, sourceCT);

                output = "Operation successfully completed.";
                Log( output, false );
                return 0;
            }
            catch (Exception ex) {
                output = "Unhandled error occured: " + ex.Message;
                Log(output, false);
                return -1;
            }
            finally {
                if (rootWeb != null) {
                    rootWeb.Dispose();
                }
                if (siteCollection != null) {
                    siteCollection.Dispose();
                }
            }
        }

        /// 
        /// Go through a web, all lists and sync with the source content 
        /// type.
        /// Go recursively through all sub webs.
        /// 
        /// 
        /// 
        private void ProcessWeb(SPWeb web, SPContentType sourceCT) {
            //Do work on lists on this web
            Log("Processing web: " + web.Url);

            //Grab the lists first, to avoid messing up an enumeration 
            // while looping through it.
            List lists = new List();
            foreach (SPList list in web.Lists) {
                lists.Add(list.ID);
            }

            foreach (Guid listId in lists) {
                SPList list = web.Lists[listId];

                if (list.ContentTypesEnabled) {
                    Log("Processing list: " + list.ParentWebUrl + "/" 
                         + list.Title);

                    SPContentType listCT = 
                                         list.ContentTypes[ContentTypeName];
                    if (listCT != null) {
                        Log("Processing content type on list:" + list);

                        if (UpdateFields) {
                          UpdateListFields(list, listCT, sourceCT);
                        }

                        //Find/add the fields to add
                        foreach (SPFieldLink sourceFieldLink in 
                                               sourceCT.FieldLinks) {
                          if (!FieldExist(sourceCT, sourceFieldLink)) {
                            Log(
                              "Failed to add field " 
                              + sourceFieldLink.DisplayName + " on list " 
                              + list.ParentWebUrl + "/" + list.Title 
                              + " field does not exist (in .Fields[]) on " 
                              + "source content type", false);
                          }
                          else {
                            if (!FieldExist(listCT, sourceFieldLink)) {
                              //Perform double update, just to be safe 
                              // (but slow)
                              Log("Adding field \"" 
                                 + sourceFieldLink.DisplayName 
                                 + "\" to contenttype on " 
                                 + list.ParentWebUrl + "/" + list.Title, 
                                   false);
                              if (listCT.FieldLinks[sourceFieldLink.Id] 
                                                                != null) {
                                listCT.FieldLinks.Delete(sourceFieldLink.Id);
                                listCT.Update();
                              }
                              listCT.FieldLinks.Add(new SPFieldLink(
                                      sourceCT.Fields[sourceFieldLink.Id]));
                              listCT.Update();
                            }
                          }
                        }


                      if (RemoveFields) {
                            //Find the fields to delete
                            //WARNING: this part of the code has not been 
                            // adequately tested (though
                            // what could go wrong? ;-) ... )

                            //Copy collection to avoid modifying enumeration
                            // as we go through it
                            List listFieldLinks = 
                                                  new List();
                            foreach (SPFieldLink listFieldLink in 
                                                     listCT.FieldLinks) {
                                listFieldLinks.Add(listFieldLink);
                            }

                            foreach (SPFieldLink listFieldLink in 
                                                        listFieldLinks) {
                                if (!FieldExist(sourceCT, listFieldLink)) {
                                    Log("Removing field \"" 
                                       + listFieldLink.DisplayName 
                                       + "\" from contenttype on :" 
                                       + list.ParentWebUrl + "/" 
                                       + list.Title, false);
                                    listCT.FieldLinks.Delete(
                                                        listFieldLink.Id);
                                    listCT.Update();
                                }
                            }
                        }
                    }
                }
            }


            //Process sub webs
            foreach (SPWeb subWeb in web.Webs) {
                ProcessWeb(subWeb, sourceCT);
                subWeb.Dispose();
            }
        }


      /// 
      /// Updates the fields of the list content type (listCT) with the 
      /// fields found on the source content type (courceCT).
      /// 
      /// 
      /// 
      /// 
      private void UpdateListFields(SPList list, SPContentType listCT, 
                                    SPContentType sourceCT) {
        Log("Starting to update fields ", false);
        foreach (SPFieldLink sourceFieldLink in sourceCT.FieldLinks) {
          //has the field changed? If not, continue.
          if (listCT.FieldLinks[sourceFieldLink.Id]!= null 
               && listCT.FieldLinks[sourceFieldLink.Id].SchemaXml 
                  == sourceFieldLink.SchemaXml) {
            Log("Doing nothing to field \"" + sourceFieldLink.Name 
                + "\" from contenttype on :" + list.ParentWebUrl + "/" 
                + list.Title, false);
            continue;
          }
          if (!FieldExist(sourceCT, sourceFieldLink)) {
            Log(
              "Doing nothing to field: " + sourceFieldLink.DisplayName 
               + " on list " + list.ParentWebUrl 
               + "/" + list.Title + " field does not exist (in .Fields[])"
               + " on source content type", false);
            continue;
                              
          }

          if (listCT.FieldLinks[sourceFieldLink.Id] != null) {

            Log("Deleting field \"" + sourceFieldLink.Name 
                + "\" from contenttype on :" + list.ParentWebUrl + "/" 
                + list.Title, false);

            listCT.FieldLinks.Delete(sourceFieldLink.Id);
            listCT.Update();
          }

          Log("Adding field \"" + sourceFieldLink.Name 
              + "\" from contenttype on :" + list.ParentWebUrl 
              + "/" + list.Title, false);

          listCT.FieldLinks.Add(new SPFieldLink(
                                     sourceCT.Fields[sourceFieldLink.Id]));
          //Set displayname, not set by previus operation
          listCT.FieldLinks[sourceFieldLink.Id].DisplayName 
                      = sourceCT.FieldLinks[sourceFieldLink.Id].DisplayName;
          listCT.Update();
          Log("Done updating fields ");
        }
      }

      private static bool FieldExist(SPContentType contentType, 
                                                     SPFieldLink fieldLink)
        {
            try
            {
                //will throw exception on missing fields
                return contentType.Fields[fieldLink.Id] != null;
            }
            catch(Exception)
            {
                return false;
            }
        }

        private void Log(string str, bool verboseLevel) {
            if (Verbose || !verboseLevel) {
                Console.WriteLine(str);
            }
        }

        private void Log(string str) {
            Log(str, true);
        }


        /// 
        /// Parse the input settings
        /// 
        /// 
        /// 
        private bool GetSettings(StringDictionary keyValues) {
            try {
                ProvidedUrl = keyValues["url"];
                //test the url
                new Uri(ProvidedUrl);

                ContentTypeName = keyValues["contenttype"];
                if (string.IsNullOrEmpty(ContentTypeName)) {
                    throw new ArgumentException("contenttype missing");
                }

                if (keyValues.ContainsKey("removefields")) {
                    RemoveFields = true;
                }

                if (keyValues.ContainsKey("verbose")) {
                    Verbose = true;
                }

                if (keyValues.ContainsKey("updatefields"))
                {
                    UpdateFields = true;
                }
                return true;
            }
            catch (Exception ex) {
                Console.Out.WriteLine("An error occuring in retrieving the"
                    + " parameters. \r\n(" + ex + ")\r\n");
                return false;
            }
        }

        /// 
        /// Output help to console
        /// 
        /// 
        /// 
        public string GetHelpMessage(string command) {
            StringBuilder helpMessage = new StringBuilder();

            // syntax
            helpMessage.AppendFormat("\tstsadm -o {0}{1}{1}", command, 
                                                 Environment.NewLine);
            helpMessage.Append("\t-url " + Environment.NewLine);
            helpMessage.Append("\t-contenttype " + Environment.NewLine);
            helpMessage.Append("\t[-removefields]" + Environment.NewLine);
            helpMessage.Append("\t[-updatefields]" + Environment.NewLine);
            helpMessage.Append("\t[-verbose]" + Environment.NewLine);

            // description
            helpMessage.AppendFormat("{0}This action will propagate a site"
                + " content type to all list content types within the "
                + "site collection.{0}Information propagated is field "
                + "addition/removal.{0}{0}", Environment.NewLine);
            helpMessage.AppendFormat("{0}Søren Nielsen (soerennielsen." 
                + "wordpress.com){0}{0}", Environment.NewLine);

            return helpMessage.ToString();
        }
    }
}

Final Comments

I’ve made zero attempts to optimize the code. It doesn’t really matter how long it takes, does it? Give it 10 minutes till a couple of hours for huge site collection (I’ve tested with about 400 sub sites).

I recommend that you use the verbose flag and pipe the output to a file, so that you can review that it did everything correctly.

The code does not handle site content types on sub sites I’ll probably add it fairly soon if I need it or time permits (does it ever?)

License

Use the above code freely, with two conditions:

  1. Leave my name and link in there ;-)
  2. Report bug and improvements back to me

Convert “virtual” content types to “physical”


What do you do, if you in a fit of madness/desperation/stupidity created the content types used throughout your site, through the web interface and you now want to do the “right” thing and place them in xml files packaged as a feature?

Well this is description on how to convert the existing “virtual” content type to that xml file, while maintaining the integrity of your existing site and content. Warning: I’m modifying the SharePoint content database directly – use at your own risk!

The basic idea:

  1. Create a content type xml file and package it in a feature (don’t deploy it yet) as you would if you started in a blank environment
  2. “Steal or copy” the content id for “virtual” content type from the database and use it in your xml files. In other words the existing content id that is used throughout your existing SharePoint database in the inheritance hierarchy, will remain unchanged
  3. Modify the database so that SharePoint sees your content type as being feature based instead of “database based”
  4. Deploy your new content type feature. You can now update that content type as if you had started it out xml based to begin with

It seems fairly straightforward doesn’t it? It actually is.

Howto

Information on creating xml based content type can be found here (and on many other sources), it’s really not that hard. Your deployment will be much easier after this.

Right about now would be a good time to do a backup of your content database ;-)

Step 1: Steal the Content Type ID

Your content type will need a very specific ID that the SharePoint created for you when you created your new content type in the first place (either through the web frontend or API). It looks like “0x0101……” and will probably be a rather long string. You need to grab this id from the content database:

  1. Connect to the content database in question, probably named wss_content_XXXX (if you didn’t choose a database name the XXXX will be a guid)
  2. Execute the following query to find the right content typeselect ResourceDir, ContentTypeId, Definition

    from dbo.ContentTypes

    where ResourceDir like ‘%Article Page%’

    Obviously substitute your own content type name, note that the web interface might have appended some trailing numbers to the name, so you’ll have to do a “like” selection

  3. Copy the ContentTypeID and insert it into the xml file. You might also want to verifiy that the definition corresponds to your that in your xml file (or just copy it over)

Step 2: Connect the Content Type to the XML File

Now you need to go into the database and modify the ContentType table to make SharePoint see it as a feature based content type as opposed to those solely in the database.

  1. Connect to the content database again (you might just have kept the window open)
  2. Execute begin tran once, just to give you an undo option
  3. Execute the following SQL statementUpdate dbo.ContentTypes

    Set Size = 16, Definition = null, IsFromFeature = true

    where ContentTypeId = 0x010100C5…..

    It should only modify one row

  4. If the name “ResourceDir” has been mangled by the web interface, you might want to take the opportunity to fix that too now
  5. If you are satisfied with the update execute commit tran, otherwise rollback tran, do not forget this as you are locking the table for the duration (btw: Isn’t that a neat trick?)

Caveats

I will not take any responsibility if you lose your databases, however I would like to know if you find flaws with the procedure ;-)

If you have many environments this technique only works if they have the same content type id for the same type across the farms. They will have if you did a backup/restore or content deployment from one to the other. They won’t if you created them through the web on both servers. Then you either choose which one is the master of the content or you are out of luck.

Note that if you update/change the content type xml files at a later time, the changes will only apply to the site scoped content type, not the actual list content types that the system created for every list where the type is enabled. This is very bad news, but not to worry I’ll post the fix for that in a few days (give me a bit of time).

If you modify the content type through the web interface after deployment it will once again be disconnected from the xml source, and you’ll have to complete Step 2 (only) to reconnect it.

Follow

Get every new post delivered to your Inbox.