gl-fixpagecontact Deprecated!!!
Woohoo! – I finally get to deprecate one of my commands – after creating and publishing 135 commands I discovered today that the August Cumulative Update fixes the issue that my gl-fixpagecontact command sought to address. If you look back at the post for that command you can see that there was a bug with the product which resulted in a user not found exception being thrown when looking at the settings of a publishing page. This happened in couple of different situations but the easiest way to trigger it was to simply delete the user that created the page (or that was assigned as the page contact or that last modified the page) from the site collection. When you did this and then visited the settings page you would get an error similar to the following:
The error would vary slightly depending on whether or not the field was the “Contact”, “CreatedBy”, or “LastModifiedBy” field. I decided to revisit this command due to a comment that was posted on the post related to it which asked if the command could be modified to handle the CreatedBy and LastModifiedBy fields (I was only handling the Contact field). I did some digging and have not yet discovered a way to update these two fields for a document library (if anyone has a way to do this please let me know) so I decided to send an email out to Paul Andrew to see if the bug had already been addressed - I usually pay attention to what fixes went into each update but I guess I missed this because within just a few minutes I got a response from Paul stating that this was indeed fixed with the August update (http://support.microsoft.com/kb/956056 and http://support.microsoft.com/kb/956057). So I decided to go ahead and update my development machine to confirm this and Merry Christmas it worked! After the updates got installed the settings page loaded fine and the invalid fields were set to my system account – now I’m not sure what logic is used here to determine what user is used – I was logged in with my system account so I don’t know if it’s using the logged in user or the system account but regardless, the page loads and that’s the important part.
So I am now officially deprecating this command – I’ll keep it in with the package but I won’t be supporting it further now that I can finally tell people to just install the update! Thank you Paul for your help and quick responses!!!
Fixing Invalid Page Contacts
Update 12/29/2008: This command has been deprecated – the issues described below have been resolved with the August 2008 Cumulative Update (http://support.microsoft.com/kb/956056 and http://support.microsoft.com/kb/956057).
I've been doing a lot of migration work these days and if you've ever had to migrate content from one farm (or site collection) to another you know that all sorts of things can go wrong (many of which have been documented in one form or another on this blog). One of the issues I've recently come across is related to the contact settings for a publishing page. There are two ways in which this setting can become invalid and in both circumstances there is no way to fix the problem without using either SharePoint Designer or code (the invalid contact results in an error being thrown when trying to view the Page Settings and Schedules page).
The first way to end up with an invalid contact is to simply delete the contact from the list of users in the site collection. Now, whether or not you should ever delete users is a topic for a different post but if you delete the contact that is associated with a page and then try to view the Page Settings and Schedule page you will see the following "User cannot be found" error:
It's pretty frustrating that the page isn't sensitive enough to realize the contact is invalid and therefore either clear the setting or display some warning but at least let me load the page so that I can fix the error without having to crack open SPD.
The second way that this error can manifest itself is via a content migration. For example - you have a valid contact assigned to the page in your test or staging environment and then you migrate the page to your production environment which is on a different domain and thus the page contact does not exist in that domain. This is the scenario that I've been dealing with and because I've been moving pages in bulk I needed a better way than to use SPD every time I migrated a page - I wanted to be able to fix the issues as part of my deployment script. The result is a new command I created called gl-fixpagecontact.
To better understand why the contact can become invalid (even in a migration scenario where your usernames are the same in both domains) it's important to understand how the information is stored. The contact information is stored in the "Contact" (FieldId.Contact) field of the pages SPListItem object and the format is the users ID followed by ";#" (without the quotes) followed by the users display name: 20;#Test User
The ID shown is (obviously) not the SID of the user but rather the ID of the user in the site collections users list and obviously this ID will vary from one farm to another (and even from one site collection to another). Note that if you delete a user from the site collection and then add them back in their ID will be set to the original ID so to fix the first scenario you can simply re-add the user, fix the contact for the page manually and then re-delete the user.
So what does my new command do? It parses the data in the field (splits on ;#) and uses the display name to try and find a principle that matches the principle regardless of the ID. If it can't find a match then it will replace the contact with either the current user or a user provided via the "-contact" parameter. Note that you can also use this command to force all pages to have a contact set - it is not invalid to have an empty contact but your business rules may require that all pages have a contact set. If you don't pass the "-allowempty" flag it will force the contact to be set to either the current user or a named user (via the -contact parameter).
Here's the syntax of the command:
C:\>stsadm -help gl-fixpagecontact
stsadm -o gl-fixpagecontact
Fixes the Page Contact property of a publishing page or pages if the current contact is invalid.
Parameters:
-url <url>
-scope <WebApplication | Site | Web>
[-pagename <the name of the page to update>]
[-contact <DOMAIN\name (contact user name to assign to the page - if not provided the current user is used)>]
[-allowempty (allow empty contact values)]
[-verbose]
[-test]
|
Like many of my commands that do mass updates of data I provide a "-test" parameter so that you can simulate the execution and see what it would have changed without having it actually make changes - this is useful for just identifying your problem pages. Here's a simple example of how you would run this command to fix all pages in a given site collection:
stsadm -o gl-fixpagecontact -url http://demo -scope site -allowempty -verbose
If you want to just fix a single page in a single web you would run the following:
stsadm -o gl-fixpagecontact -url http://demo -scope web -pagename default.aspx -verbose
Set the Welcome Page for a Web
When I did the gradual upgrade I found that several sites had the welcome page set to UpgLandingPgRedir.aspx when it should have been set to default.aspx. I didn't have time to figure out why it was doing this but the fix was easy enough - I just had to change the welcome page back. To do this I created a new command: gl-sitewelcomepage. The code to set this is really simple - basically just get a PublishingWeb instance and set the DefaultPage property to a valid SPFile object:
1: string url = Params["site"].Value.TrimEnd('/');
2: string page = SPHttpUtility.UrlPathDecode(Params["welcomepage"].Value, true);
3:
4: using (SPSite site = new SPSite(url))
5: using (SPWeb web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(url)])
6: {
7: if (!PublishingWeb.IsPublishingWeb(web))
8: throw new SPException("The specified site is not a publishing web.");
9:
10: PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(web);
11:
12: bool flag = false;
13: SPFile file = null;
14: try
15: {
16: file = pubWeb.Web.GetFile(page);
17: flag = file.Exists;
18: }
19: catch (SPException)
20: {
21: flag = false;
22: }
23: catch (FileNotFoundException)
24: {
25: flag = false;
26: }
27: catch (ArgumentException)
28: {
29: flag = false;
30: }
31:
32: if (!flag || file == null)
33: throw new SPException("The specified welcome page could not be found.");
34:
35:
36: PublishingWeb currentPublishingWeb = pubWeb;
37: currentPublishingWeb.DefaultPage = file;
38: currentPublishingWeb.Update();
39: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-sitewelcomepage
stsadm -o gl-sitewelcomepage
Sets the page to be used as the welcome page for the site.
Parameters:
-site <url of the site to update>
-welcomepage <full url to the page to use as the welcome page>
Here's an example of how to set the welcome page for a site:
List All Welcome Pages
After I had created the gl-sitewelcomepage command I needed a quick way to find all the pages that were pointing to the wrong place. To do this I hacked out this command which essentially just loops through a farm, web application, site collection or web and displays what the current welcome page is set to for each web. The command is called gl-enumwelcomepages. Like the gl-sitewelcomepage command this command is really simple - most of the code is focused on looping through the various objects to get to the PublishingWeb object:
1: /// <summary>
2: /// Runs the specified command.
3: /// </summary>
4: /// <param name="command">The command.</param>
5: /// <param name="keyValues">The key values.</param>
6: /// <param name="output">The output.</param>
7: /// <returns></returns>
8: public override int Run(string command, StringDictionary keyValues, out string output)
9: {
10: output = string.Empty;
11:
12: InitParameters(keyValues);
13:
14: string url = Params["url"].Value;
15: if (url != null)
16: url = url.TrimEnd('/');
17:
18: string scope = Params["scope"].Value.ToLowerInvariant();
19:
20: if (scope == "farm")
21: {
22: foreach (SPService svc in SPFarm.Local.Services)
23: {
24: if (!(svc is SPWebService))
25: continue;
26:
27: foreach (SPWebApplication webApp in ((SPWebService)svc).WebApplications)
28: {
29: DisplayWelcomePageUrl(webApp);
30: }
31: }
32: }
33: else if (scope == "webapplication")
34: {
35: SPWebApplication webApp = SPWebApplication.Lookup(new Uri(url));
36: DisplayWelcomePageUrl(webApp);
37: }
38: else if (scope == "site")
39: {
40: using (SPSite site = new SPSite(url))
41: {
42: DisplayWelcomePageUrl(site);
43: }
44: }
45: else if (scope == "web")
46: {
47: using (SPSite site = new SPSite(url))
48: using (SPWeb web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(url)])
49: {
50: DisplayWelcomePageUrl(site, web, true);
51: }
52: }
53:
54: return 1;
55: }
56:
57:
58: /// <summary>
59: /// Validates the specified key values.
60: /// </summary>
61: /// <param name="keyValues">The key values.</param>
62: public override void Validate(StringDictionary keyValues)
63: {
64: if (Params["scope"].UserTypedIn)
65: {
66: if (Params["scope"].Value.ToLowerInvariant() == "farm" && Params["url"].UserTypedIn)
67: throw new SPSyntaxException("The url parameter is not compatible with a scope of Farm.");
68: if (Params["scope"].Value.ToLowerInvariant() != "farm" && !Params["url"].UserTypedIn)
69: throw new SPSyntaxException("The url parameter is required if the scope is not Farm.");
70: }
71: base.Validate(keyValues);
72: }
73:
74: #endregion
75:
76:
77: /// <summary>
78: /// Displays the welcome page URL.
79: /// </summary>
80: /// <param name="webApp">The web app.</param>
81: private static void DisplayWelcomePageUrl(SPWebApplication webApp)
82: {
83: foreach (SPSite site in webApp.Sites)
84: {
85: try
86: {
87: DisplayWelcomePageUrl(site);
88: }
89: finally
90: {
91: site.Dispose();
92: }
93: }
94: }
95:
96: /// <summary>
97: /// Displays the welcome page URL.
98: /// </summary>
99: /// <param name="site">The site.</param>
100: private static void DisplayWelcomePageUrl(SPSite site)
101: {
102: foreach (SPWeb web in site.AllWebs)
103: {
104: try
105: {
106: DisplayWelcomePageUrl(site, web, false);
107: }
108: finally
109: {
110: web.Dispose();
111: }
112: }
113: }
114:
115: /// <summary>
116: /// Displays the welcome page URL.
117: /// </summary>
118: /// <param name="site">The site.</param>
119: /// <param name="web">The web.</param>
120: /// <param name="recurseSubWebs">if set to <c>true</c> [recurse sub webs].</param>
121: internal static void DisplayWelcomePageUrl(SPSite site, SPWeb web, bool recurseSubWebs)
122: {
123: if (recurseSubWebs)
124: {
125: foreach (SPWeb subweb in web.Webs)
126: {
127: DisplayWelcomePageUrl(site, subweb, recurseSubWebs);
128: }
129: }
130:
131: if (!PublishingWeb.IsPublishingWeb(web))
132: return;
133:
134: PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(web);
135:
136: Console.WriteLine(site.MakeFullUrl(web.ServerRelativeUrl) + " = " + pubWeb.DefaultPage);
137: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-enumwelcomepages
stsadm -o gl-enumwelcomepages
Lists all the welcome page(s) for a farm, web application, site collection, or web.
Parameters:
-url <url to search>
-scope <Farm | WebApplication | Site | Web>
Here's an example of how to list all the welcome pages in a web application:
Enumerate Page Web Parts
As part of the upgrade I needed to be able to fix some web parts that did not migrate correctly (either during the upgrade itself or as a result of moving a web). Before I started messing around with the web parts though I wanted to be able to see what I was dealing with. So I decided to create this simple command called gl-enumpagewebparts that would enable me to list out in XML all the web parts that are on a given page (open or closed).
One thing that I found that was very interesting was that the web part manager export method treats V2 and V3 web parts very differently. But perhaps the biggest annoyance I found was that I couldn't get the web part zone from the web part instance itself - I had to use the web part manager (SPLimitedWebPartManager) to get it (took me longer than I'd like to admit to figure that out). This command is really quite simple - it takes in an url to a web part page, loads up an SPLimitedWebPartManager (for both the shared and personal views) and then loops through the WebParts collection outputting the results as XML.
I created three separate methods to get the XML details - one is verbose and essentially just uses the built in Export() method to get the XML (you can get these results via the -verbose parameter), another is a bit simpler and is constructed by hand (this is the default) and a third is actually for use by another command that I created which I'll be documenting soon. The core code is shown below:
1: /// <summary>
2: /// Runs the specified command.
3: /// </summary>
4: /// <param name="command">The command.</param>
5: /// <param name="keyValues">The key values.</param>
6: /// <param name="output">The output.</param>
7: /// <returns></returns>
8: public override int Run(string command, StringDictionary keyValues, out string output)
9: {
10: output = string.Empty;
11:
12: InitParameters(keyValues);
13:
14: string url = Params["url"].Value;
15:
16: XmlDocument xmlDoc = new XmlDocument();
17: xmlDoc.AppendChild(xmlDoc.CreateElement("WebParts"));
18: xmlDoc.DocumentElement.SetAttribute("page", url);
19:
20: using (SPSite site = new SPSite(url))
21: using (SPWeb web = site.OpenWeb()) // The url contains a filename so AllWebs[] will not work unless we want to try and parse which we don't
22: {
23: XmlElement shared = xmlDoc.CreateElement("Shared");
24: xmlDoc.DocumentElement.AppendChild(shared);
25:
26: SPLimitedWebPartManager webPartMngr = web.GetLimitedWebPartManager(url, PersonalizationScope.Shared);
27:
28: string tempXml = string.Empty;
29: foreach (WebPart wp in webPartMngr.WebParts)
30: {
31: if (Params["verbose"].UserTypedIn)
32: tempXml += GetWebPartDetails(wp, webPartMngr);
33: else
34: tempXml += GetWebPartDetailsSimple(wp, webPartMngr);
35: }
36: shared.InnerXml = tempXml;
37:
38: XmlElement user = xmlDoc.CreateElement("User");
39: xmlDoc.DocumentElement.AppendChild(user);
40:
41: webPartMngr = web.GetLimitedWebPartManager(url, PersonalizationScope.User);
42: tempXml = string.Empty;
43: foreach (WebPart wp in webPartMngr.WebParts)
44: {
45: if (Params["verbose"].UserTypedIn)
46: tempXml += GetWebPartDetails(wp, webPartMngr);
47: else
48: tempXml += GetWebPartDetailsSimple(wp, webPartMngr);
49: }
50: user.InnerXml = tempXml;
51:
52: }
53:
54: output += Utilities.GetFormattedXml(xmlDoc);
55:
56: return 1;
57: }
58:
59: #endregion
60:
61: /// <summary>
62: /// Gets the web part details.
63: /// </summary>
64: /// <param name="wp">The web part.</param>
65: /// <param name="manager">The web part manager.</param>
66: /// <returns></returns>
67: internal static string GetWebPartDetails(WebPart wp, SPLimitedWebPartManager manager)
68: {
69: StringBuilder sb = new StringBuilder();
70:
71: XmlTextWriter xmlWriter = new XmlTextWriter(new StringWriter(sb));
72: xmlWriter.Formatting = Formatting.Indented;
73: manager.ExportWebPart(wp, xmlWriter);
74: xmlWriter.Flush();
75:
76: XmlDocument xmlDoc = new XmlDocument();
77: xmlDoc.LoadXml(sb.ToString());
78:
79: XmlElement elem = xmlDoc.DocumentElement;
80: if (xmlDoc.DocumentElement.Name == "webParts")
81: {
82: elem = (XmlElement)xmlDoc.DocumentElement.ChildNodes[0];
83:
84: // We've found a v3 web part but the export method does not export what the zone ID is so we
85: // have to manually add that in. Unfortunately the Zone property is always null because we are
86: // using a SPLimitedWebPartManager so we have to use the helper method GetZoneID to set the zone ID.
87: XmlElement property = xmlDoc.CreateElement("property");
88: property.SetAttribute("name", "ZoneID");
89: property.SetAttribute("type", "string");
90: property.InnerText = manager.GetZoneID(wp);
91: elem.ChildNodes[1].ChildNodes[0].AppendChild(property);
92: }
93:
94: return elem.OuterXml.Replace(" xmlns=\"\"", ""); // Just some minor cleanup to deal with erroneous namespace tags added due to the zoneID being added manually.
95: }
The syntax of the command I created can be seen below.
C:\>stsadm -help gl-enumpagewebparts
stsadm -o gl-enumpagewebparts
Lists all the web parts that have been added to the specified page.
Parameters:
-url <web part page URL>
[-verbose]
Here’s an example of how to list all the web parts on a given page and dump to a text file:
stsadm -o gl-enumpagewebparts -url "http://intranet/hr/pages/default.aspx" -verbose > webparts.xml
Re-Ghosting Pages
After running a test upgrade I discovered that one page particular was showing up as un-ghosted (or customized) despite my setting the option to reset all pages to the site definition when I ran the upgrade. I attempted to use the browser to reset the page (in this case http://intranet/sitedirectory/lists/sites/summary.aspx) but that had no affect.
I decided that I needed to get more information about how the un-ghosting process works. The first thing I needed to do was see if there were any other pages with the same problem. To do this I created a command called gl-enumunghostedfiles. I know there are versions of the same command out there already but I found I needed something a bit more capable so that I could search an entire site collection and not just a single web.
After creating this command (detailed below) I was surprised to see that this summary.aspx page was not showing up as un-ghosted at all. When looking for an un-ghosted file you typically just check the CustomizedPageStatus property of an SPFile object. If this returns back as Customized (so much better than saying "un-ghosted" - not sure why this term came up and why I'm proliferating it
) then the page is un-ghosted. If you look internally at how this property is evaluated you'll see that the code is checking for the presense of a property in the Properties collection called "vti_setuppath" - if this property is not null or empty then it checks for another property called "vti_hasdefaultcontent" and if it either doesn't find this or it's set to false then it returns back a value of Customized. You can see this in the code below:
1: public SPCustomizedPageStatus CustomizedPageStatus
2: {
3: get
4: {
5: if (!string.IsNullOrEmpty(this.SetupPath))
6: {
7: bool flag = false;
8: try
9: {
10: object obj2 = this.Properties["vti_hasdefaultcontent"];
11: if (obj2 != null)
12: {
13: flag = bool.Parse((string) obj2);
14: }
15: }
16: catch (FormatException)
17: {
18: }
19: if (flag)
20: {
21: return SPCustomizedPageStatus.Uncustomized;
22: }
23: return SPCustomizedPageStatus.Customized;
24: }
25: return SPCustomizedPageStatus.None;
26: }
27: }
What I found when I looked closer at this page that I knew (based on the shear appearance of the page) was un-ghosted was that the "vti_hasdefaultcontent" property was set to "true" and the "vti_setuppath" property was set to the old 2003 template path. I spent many hours trying to figure out how to re-ghost this page and ended up ultimately unsuccessful. If anyone has any thoughts on this I'd love to hear them (I've tried updating the SetupPath and SetupPathVerion fields in the AllDocs table to point the file to the new template but that just resulted in an unknown error when loading the page - changing the "vti_hasdefaultcontent" property manually also didn't work as the value would not persist and I couldn't find where it's stored in the DB and changing this value in memory would allow the SPRequest.RevertContentStreams() method to be called but that would just throw a file not found exception as it is unable to locate the template file despite copying the file to various suspect locations).
As a result of my efforts though I do have a fairly robust command to re-ghost pages which does seem to work with another issue I encountered. I found that when I imported a list from another site the pages (views) for the list were showing up as un-ghosted but when I tried to use the RevertContentStream method of the SPFile object it had no affect.
After messing around with it for a while I discovered that if I called the internal SPRequest.RevertContentStreams() method directly and did not follow that call up with the call to SPRequest.UpdateFileOrFolderProperties() as the SPFile.RevertContentStream() method does then I can get the file to successfully be re-ghosted. I've got an email out to Microsoft to see if they can explain why this is so but in the mean-time it seems to work. The two commands that I created are detailed further below.
1. gl-enumunghostedfiles
The code for this command is pretty simple - I've basically just got two recursive methods, one for the web sites and another for folders. I allowed a parameter to be passed in to determine whether the code should recurse sub webs or not. The code is shown below:
1: public class EnumUnGhostedFiles : SPOperation
2: {
3: /// <summary>
4: /// Initializes a new instance of the <see cref="EnumUnGhostedFiles"/> class.
5: /// </summary>
6: public EnumUnGhostedFiles()
7: {
8: SPParamCollection parameters = new SPParamCollection();
9: parameters.Add(new SPParam("url", "url", true, null, new SPUrlValidator(), "Please specify the site url."));
10: parameters.Add(new SPParam("recursesubwebs", "recurse", false, null, null));
11: Init(parameters, "\r\n\r\nReturns a list of all unghosted (customized) files for a web.\r\n\r\nParameters:\r\n\t-url <web site url>\r\n\t[-recursesubwebs]");
12: }
13:
14: #region ISPStsadmCommand Members
15:
16: /// <summary>
17: /// Gets the help message.
18: /// </summary>
19: /// <param name="command">The command.</param>
20: /// <returns></returns>
21: public override string GetHelpMessage(string command)
22: {
23: return HelpMessage;
24: }
25:
26: /// <summary>
27: /// Runs the specified command.
28: /// </summary>
29: /// <param name="command">The command.</param>
30: /// <param name="keyValues">The key values.</param>
31: /// <param name="output">The output.</param>
32: /// <returns></returns>
33: public override int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
34: {
35: output = string.Empty;
36:
37: InitParameters(keyValues);
38:
39: string url = Params["url"].Value;
40: bool recurse = Params["recursesubwebs"].UserTypedIn;
41: List<string> unghostedFiles = new List<string>();
42:
43: using (SPSite site = new SPSite(url))
44: {
45: using (SPWeb web = site.OpenWeb())
46: {
47: if (recurse)
48: {
49: RecurseSubWebs(web, ref unghostedFiles);
50: }
51: else
52: CheckFoldersForUnghostedFiles(web.RootFolder, ref unghostedFiles);
53: }
54: }
55:
56: if (unghostedFiles.Count == 0)
57: {
58: output += "There are no unghosted (customized) files on the current web.\r\n";
59: }
60: else
61: {
62: output += "The following files are unghosted:";
63:
64: foreach (string fileName in unghostedFiles)
65: {
66: output += "\r\n\t" + fileName;
67: }
68: }
69:
70: return 1;
71: }
72:
73: #endregion
74:
75: /// <summary>
76: /// Recurses the sub webs.
77: /// </summary>
78: /// <param name="web">The web.</param>
79: /// <param name="unghostedFiles">The unghosted files.</param>
80: private static void RecurseSubWebs(SPWeb web, ref List<string>unghostedFiles)
81: {
82: foreach (SPWeb subweb in web.Webs)
83: {
84: try
85: {
86: RecurseSubWebs(subweb, ref unghostedFiles);
87: }
88: finally
89: {
90: subweb.Dispose();
91: }
92: }
93: CheckFoldersForUnghostedFiles(web.RootFolder, ref unghostedFiles);
94: }
95:
96: /// <summary>
97: /// Checks the folders for unghosted files.
98: /// </summary>
99: /// <param name="folder">The folder.</param>
100: /// <param name="unghostedFiles">The unghosted files.</param>
101: private static void CheckFoldersForUnghostedFiles(SPFolder folder, ref List<string> unghostedFiles)
102: {
103: foreach (SPFolder sub in folder.SubFolders)
104: {
105: CheckFoldersForUnghostedFiles(sub, ref unghostedFiles);
106: }
107:
108: foreach (SPFile file in folder.Files)
109: {
110: if (file.CustomizedPageStatus == SPCustomizedPageStatus.Customized)
111: {
112: if (!unghostedFiles.Contains(file.ServerRelativeUrl))
113: unghostedFiles.Add(file.ServerRelativeUrl);
114: }
115: }
116: }
117: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-enumunghostedfiles
stsadm -o gl-enumunghostedfiles
Returns a list of all unghosted (customized) files for a web.
Parameters:
-url <web site url>
[-recursesubwebs]
The following table summarizes the command and its various parameters:
| Command Name | Availability | Build Date |
|---|---|---|
| gl-enumunghostedfiles | WSS v3, MOSS 2007 | Released: 9/13/2007
|
| Parameter Name | Short Form | Required | Description | Example Usage |
|---|---|---|---|---|
| url | Yes | URL to analyze. | -url http://intranet/ | |
| recursesubwebs | recurse | No | If not specified then only the single web will be considered. To recurse the web and all it’s sub-webs pass in this parameter. | -recursesubwebs
-recurse |
Here’s an example of how to return the un-ghosted files for a root site collection and all sub-webs:
stsadm –o gl-enumunghostedfiles -url "http://intranet/" -recursesubwebs
You can see a sample of what running the above command will produce - your results will most likely be very different:
C:\>stsadm –o gl-enumunghostedfiles -url "http://intranet/" -recursesubwebs
The following files are unghosted:
/News/Pages/Default.aspx
/Reports/Pages/default.aspx
/SearchCenter/Pages/people.aspx
/SearchCenter/Pages/default.aspx
/SearchCenter/Pages/peopleresults.aspx
/SearchCenter/Pages/results.aspx
/SearchCenter/Pages/advanced.aspx
/SiteDirectory/Pages/category.aspx
/SiteDirectory/Pages/sitemap.aspx
/Pages/Default.aspx
/FormServerTemplates/Forms/InfoPath Form Template/template.doc
/Variation Labels/NewForm.aspx
/Variation Labels/EditForm.aspx
/Variation Labels/AllItems.aspx
/Variation Labels/DispForm.aspx
/_catalogs/masterpage/VariationRootPageLayout.aspx
/_catalogs/wp/siteFramer.dwp
/_catalogs/wp/IViewWebPart.dwp
/_catalogs/wp/IndicatorWebPart.dwp
/_catalogs/wp/BusinessDataFilter.dwp
/_catalogs/wp/KpiListWebPart.dwp
/_catalogs/wp/SummaryLink.webpart
/_catalogs/wp/ContentQuery.webpart
/_catalogs/wp/ThisWeekInPictures.DWP
/_catalogs/wp/SearchBestBets.webpart
/_catalogs/wp/CategoryWebPart.webpart
/_catalogs/wp/QueryStringFilter.webpart
/_catalogs/wp/SpListFilter.dwp
/_catalogs/wp/WSRPConsumerWebPart.dwp
/_catalogs/wp/searchpaging.dwp
/_catalogs/wp/contactwp.dwp
/_catalogs/wp/searchstats.dwp
/_catalogs/wp/UserContextFilter.webpart
/_catalogs/wp/owacontacts.dwp
/_catalogs/wp/SearchCoreResults.webpart
/_catalogs/wp/BusinessDataActionsWebPart.dwp
/_catalogs/wp/owainbox.dwp
/_catalogs/wp/FilterActions.dwp
/_catalogs/wp/AdvancedSearchBox.dwp
/_catalogs/wp/BusinessDataAssociationWebPart.webpart
/_catalogs/wp/RssViewer.webpart
/_catalogs/wp/owatasks.dwp
/_catalogs/wp/SearchHighConfidence.webpart
/_catalogs/wp/PeopleSearchBox.dwp
/_catalogs/wp/SearchActionLinks.webpart
/_catalogs/wp/PageContextFilter.webpart
/_catalogs/wp/owacalendar.dwp
/_catalogs/wp/AuthoredListFilter.webpart
/_catalogs/wp/searchsummary.dwp
/_catalogs/wp/CategoryResultsWebPart.webpart
/_catalogs/wp/owa.dwp
/_catalogs/wp/OlapFilter.dwp
/_catalogs/wp/BusinessDataDetailsWebPart.webpart
/_catalogs/wp/TableOfContents.webpart
/_catalogs/wp/BusinessDataListWebPart.webpart
/_catalogs/wp/TasksAndTools.webpart
/_catalogs/wp/Microsoft.Office.Excel.WebUI.dwp
/_catalogs/wp/DateFilter.dwp
/_catalogs/wp/TextFilter.dwp
/_catalogs/wp/SearchBox.dwp
/_catalogs/wp/BusinessDataItemBuilder.dwp
/_catalogs/wp/TopSitesWebPart.webpart
/_catalogs/wp/PeopleSearchCoreResults.webpart
2. gl-reghostfile
This command takes what should be a very simple call to SPFile.RevertContentStream() and attempts to handle those odd cases that I outlined above. Therefore the code is a bit of a mess and frankly nothing I'm proud of (mainly because I'm pissed I wasn't able to solve the problem). The code, which uses some reflection in order to utilize some internal objects, is shown below (sorry about the poor formatting - this blog template is less than ideal for code samples):
1: /// <summary>
2: /// Runs the specified command.
3: /// </summary>
4: /// <param name="command">The command.</param>
5: /// <param name="keyValues">The key values.</param>
6: /// <param name="output">The output.</param>
7: /// <returns></returns>
8: public override int Execute(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
9: {
10: output = string.Empty;
11: Verbose = true;
12:
13:
14: string url = Params["url"].Value;
15: bool force = Params["force"].UserTypedIn;
16: string scope = Params["scope"].Value.ToLowerInvariant();
17: bool haltOnError = Params["haltonerror"].UserTypedIn;
18:
19: switch (scope)
20: {
21: case "file":
22: using (SPSite site = new SPSite(url))
23: using (SPWeb web = site.OpenWeb())
24: {
25: SPFile file = web.GetFile(url);
26: if (file == null)
27: {
28: throw new FileNotFoundException(string.Format("File '{0}' not found.", url), url);
29: }
30:
31: Reghost(site, web, file, force, haltOnError);
32: }
33: break;
34: case "list":
35: using (SPSite site = new SPSite(url))
36: using (SPWeb web = site.OpenWeb())
37: {
38: SPList list = Utilities.GetListFromViewUrl(web, url);
39: ReghostFilesInList(site, web, list, force, haltOnError);
40: }
41: break;
42: case "web":
43: bool recurseWebs = Params["recursewebs"].UserTypedIn;
44: using (SPSite site = new SPSite(url))
45: using (SPWeb web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(url)])
46: {
47: ReghostFilesInWeb(site, web, recurseWebs, force, haltOnError);
48: }
49: break;
50: case "site":
51: using (SPSite site = new SPSite(url))
52: {
53: ReghostFilesInSite(site, force, haltOnError);
54: }
55: break;
56: case "webapplication":
57: SPWebApplication webApp = SPWebApplication.Lookup(new Uri(url));
58: Log("Progress: Analyzing files in web application '{0}'.", url);
59:
60: foreach (SPSite site in webApp.Sites)
61: {
62: try
63: {
64: ReghostFilesInSite(site, force, haltOnError);
65: }
66: finally
67: {
68: site.Dispose();
69: }
70: }
71: break;
72:
73: }
74: return OUTPUT_SUCCESS;
75: }
76:
77: #endregion
78:
79: /// <summary>
80: /// Reghosts the files in site.
81: /// </summary>
82: /// <param name="site">The site.</param>
83: /// <param name="force">if set to <c>true</c> [force].</param>
84: /// <param name="throwOnError">if set to <c>true</c> [throw on error].</param>
85: public static void ReghostFilesInSite(SPSite site, bool force, bool throwOnError)
86: {
87: Log("Progress: Analyzing files in site collection '{0}'.", site.Url);
88: foreach (SPWeb web in site.AllWebs)
89: {
90: try
91: {
92: ReghostFilesInWeb(site, web, false, force, throwOnError);
93: }
94: finally
95: {
96: web.Dispose();
97: }
98: }
99: }
100:
101: /// <summary>
102: /// Reghosts the files in web.
103: /// </summary>
104: /// <param name="site">The site.</param>
105: /// <param name="web">The web.</param>
106: /// <param name="recurseWebs">if set to <c>true</c> [recurse webs].</param>
107: /// <param name="force">if set to <c>true</c> [force].</param>
108: /// <param name="throwOnError">if set to <c>true</c> [throw on error].</param>
109: public static void ReghostFilesInWeb(SPSite site, SPWeb web, bool recurseWebs, bool force, bool throwOnError)
110: {
111: Log("Progress: Analyzing files in web '{0}'.", web.Url);
112: foreach (SPFile file in web.Files)
113: {
114: if (file.CustomizedPageStatus != SPCustomizedPageStatus.Customized && !force)
115: continue;
116:
117: Reghost(site, web, file, force, throwOnError);
118: }
119: foreach (SPList list in web.Lists)
120: {
121: ReghostFilesInList(site, web, list, force, throwOnError);
122: }
123:
124: if (recurseWebs)
125: {
126: foreach (SPWeb childWeb in web.Webs)
127: {
128: try
129: {
130: ReghostFilesInWeb(site, childWeb, true, force, throwOnError);
131: }
132: finally
133: {
134: childWeb.Dispose();
135: }
136: }
137: }
138: }
139:
140: /// <summary>
141: /// Reghosts the files in list.
142: /// </summary>
143: /// <param name="site">The site.</param>
144: /// <param name="web">The web.</param>
145: /// <param name="list">The list.</param>
146: /// <param name="force">if set to <c>true</c> [force].</param>
147: /// <param name="throwOnError">if set to <c>true</c> [throw on error].</param>
148: public static void ReghostFilesInList(SPSite site, SPWeb web, SPList list, bool force, bool throwOnError)
149: {
150: if (list.BaseType != SPBaseType.DocumentLibrary)
151: return;
152:
153: Log("Progress: Analyzing files in list '{0}'.", list.RootFolder.ServerRelativeUrl);
154:
155: foreach (SPListItem item in list.Items)
156: {
157: if (item.File == null)
158: continue;
159:
160: Reghost(site, web, item.File, force, throwOnError);
161: }
162: }
163:
164: /// <summary>
165: /// Reghosts the specified file.
166: /// </summary>
167: /// <param name="site">The site.</param>
168: /// <param name="web">The web.</param>
169: /// <param name="file">The file.</param>
170: /// <param name="force">if set to <c>true</c> [force].</param>
171: /// <param name="throwOnError">if set to <c>true</c> [throw on error].</param>
172: public static void Reghost(SPSite site, SPWeb web, SPFile file, bool force, bool throwOnError)
173: {
174: try
175: {
176: string fileUrl = site.MakeFullUrl(file.ServerRelativeUrl);
177: if (file.CustomizedPageStatus != SPCustomizedPageStatus.Customized && !force)
178: {
179: Log("Progress: " + file.ServerRelativeUrl + " was not unghosted (customized).");
180: return;
181: }
182: if (file.CustomizedPageStatus != SPCustomizedPageStatus.Customized && force)
183: {
184: if (!string.IsNullOrEmpty((string)file.Properties["vti_setuppath"]))
185: {
186: file.Properties["vti_hasdefaultcontent"] = "false";
187:
188: string setupPath = (string)file.Properties["vti_setuppath"];
189: string rootPath = SPUtility.GetGenericSetupPath("Template");
190:
191: if (!File.Exists(Path.Combine(rootPath, setupPath)))
192: {
193: string message = "The template file (" + Path.Combine(rootPath, setupPath) +
194: ") does not exist so re-ghosting (uncustomizing) will not be possible.";
195:
196: // something's wrong with the setup path - lets see if we can fix it
197: // Try and remove a leading locale if present
198: setupPath = "SiteTemplates\\" + setupPath.Substring(5);
199: if (File.Exists(Path.Combine(rootPath, setupPath)))
200: {
201: message += " It appears that a possible template match does exist at \"" +
202: Path.Combine(rootPath, setupPath) +
203: "\" however this tool currently is not able to handle pointing the file to the correct template path. This scenario is most likely due to an upgrade from SPS 2003.";
204:
205: // We found a matching file so reset the property and update the file.
206: // --- I wish this would work but it simply doesn't - something is preventing the
207: // update from occuring. Manipulating the database directly results in a 404
208: // when attempting to load the "fixed" page so there's gotta be something beyond
209: // just updating the setuppath property.
210: //file.Properties["vti_setuppath"] = setupPath;
211: //file.Update();
212: }
213: throw new FileNotFoundException(message, setupPath);
214: }
215: }
216: }
217: Log("Progress: Re-ghosting (uncustomizing) '{0}'", fileUrl);
218: file.RevertContentStream();
219:
220: file = web.GetFile(fileUrl);
221: if (file.CustomizedPageStatus == SPCustomizedPageStatus.Customized)
222: {
223: // Still unsuccessful so take measures further
224: if (force)
225: {
226: object request = Utilities.GetSPRequestObject(web);
227:
228: // I found some cases where calling this directly was the only way to force the re-ghosting of the file.
229: // I think the trick is that it's not updating the file properties after doing the revert (the
230: // RevertContentStream method will call SPRequest.UpdateFileOrFolderProperties() immediately after the
231: // RevertContentStreams call but ommitting the update call seems to make a difference.
232: Utilities.ExecuteMethod(request, "RevertContentStreams",
233: new[] { typeof(string), typeof(string), typeof(bool) },
234: new object[] { web.Url, file.Url, file.CheckOutStatus != SPFile.SPCheckOutStatus.None });
235:
236:
237: Utilities.ExecuteMethod(file, "DirtyThisFileObject", new Type[] { }, new object[] { });
238:
239: file = web.GetFile(fileUrl);
240:
241: if (file.CustomizedPageStatus == SPCustomizedPageStatus.Customized)
242: {
243: throw new SPException("Unable to re-ghost (uncustomize) file " + file.ServerRelativeUrl);
244: }
245: Log("Progress: " + file.ServerRelativeUrl + " was re-ghosted (uncustomized)!");
246: return;
247: }
248: throw new SPException("Unable to re-ghost (uncustomize) file " + file.ServerRelativeUrl);
249: }
250: Log("Progress: " + file.ServerRelativeUrl + " was re-ghosted (uncustomized)!");
251: }
252: catch (Exception ex)
253: {
254: if (throwOnError)
255: {
256: Log("ERROR:");
257: throw;
258: }
259: Log("ERROR: {0}", ex.Message);
260: }
261:
262: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-reghostfile
stsadm -o gl-reghostfile
Reghosts a file (use force to override CustomizedPageStatus check).
Parameters:
-url <url to analyze>
[-force]
[-scope <WebApplication | Site | Web | List | File>]
[-recursewebs (applies to Web scope only)]
[-haltonerror]
|
The following table summarizes the command and its various parameters:
| Command Name | Availability | Build Date |
|---|---|---|
| gl-reghostfile | WSS v3, MOSS 2007 | Released: 9/13/2007
Updated: 12/14/2008 |
| Parameter Name | Short Form | Required | Description | Example Usage |
|---|---|---|---|---|
| url | Yes | URL to analyze. If scope is “File” then URL must point to a valid file within a Web. If scope is “List” then URL must point to a valid List within a Web. | -url "http://intranet/sitedirectory/lists/sites/summary.aspx" | |
| force | No | Attempts to force the reghosting of file(s) using internal API method calls (via reflection). | -force | |
| scope | No (defaults to File) | The scope to look at when reghosting files. Valid values are “WebApplication”, “Site”, “Web”, “List”, or “File”. | -scope file | |
| recursewebs | recurse | No | Applies to “Web” scope only. If a scope of “Web” is not specified then only the single web will be considered. To recurse the web and all it’s sub-webs pass in this parameter. | -recursewebs
-recurse |
| haltonerror | halt | No | If an error occurs then stop processing other files within the specified scope. | -haltonerror
-halt |
Here’s an example of how to force the reghosting of a file:
stsadm –o gl-reghostfile -url "http://intranet/sitedirectory/lists/sites/summary.aspx" –scope file -force
If I'm able to get any answers to the issues that remain unsolved for me I'll be sure to post them here (especially if I'm able to fix the issues).