Second Life of a Hungarian SharePoint Geek

June 13, 2017

A Quick and Dirty Solution to Create a Blank Site in SharePoint 2013

Filed under: Administration, SP 2013, Tips & Tricks — Tags: , , — Peter Holpar @ 18:36

Recently one of our clients requested a change in a custom-built SharePoint application. The original version of the application was built for SharePoint (MOSS) 2007 using Visual Studio 2008, then upgraded to SharePoint 2010 using Visual Studio 2010. Later the site was upgraded to SharePoint 2013, without any change in the code of the solution.

Now we had to create a replica of the site in our developer environment including the list data. We pulled a backup of the site using the Export-SPWeb cmdlet successfully in the productive system, and created a new team site in the development system as a target of the Import-SPWeb cmdlet. When executing the restore operation we’ve got this exception:

Import-SPWeb : Cannot import site. The exported site is based on the template STS#0 but the destination site is based on the template STS#1. You can import sites only into sites that are based on same template as the exported site.

image

In the error message STS#0 means the Team Site template, and STS#1 stands for the Blank Site template (see SharePoint site template IDs and their description here). Jason Warren suggests in this thread to use the -Force switch of the Import-SPWeb cmdlet to force the overwrite of the existing site, but we had the same issue even using this switch. How could we create a new web site using the Blank Site template? Solutions available using the server side, like using PowerShell or unhiding the Blank Site template are discussed in this thread. But what could we do, if we had no access to the server side, as this site template is not available on the web UI anymore?

We found a simply solution using only a single browser (Internet Explorer in our case) and the F12 Developer Tools.

Load the site creation page in the browser, then start the Developer Tools, and select the list of templates using the DOM Explorer.

 

image

Select an options in the select element, like the Team Site

image

… change its value attribute to STS#1

image

… and finally click the Create button on the web page to create the new blank site.

This solution is quick, but I consider it to be dirty, as users should perform it themselves and each time they need a blank site, so definitely not a user friendly option. But it might be handy if you need a simple way without access to the server side.

Advertisements

February 4, 2015

How to Move all of the Site Collections of a Web Application into a Single Content Database using PowerShell

Filed under: Administration, PowerShell, SP 2010 — Tags: , , — Peter Holpar @ 23:45

Recently we had a task at one of our clients to consolidate all of the site collections of a SharePoint web application into a single content database.

There were around 10 content DBs in this web application, each of them included about 5-10 site collections. The total size of the content DBs was around 1-2 GBs, and they expected no growth in the near future.

To make the farm administration easier we had to move all of the site collections into the first content DB (let’s call it ContentDB1). We achieved this goal via the following PowerShell script:

$webAppUrl = "http://YourSPWebApp"
$destinationDBName = "ContentDB1"

$wa = Get-SPWebApplication $webAppUrl
$destinationDB = Get-SPContentDatabase -WebApplication $wa | ? { $_.Name -eq $destinationDBName }

Get-SPContentDatabase -WebApplication $wa | ? { $_.Name -ne $destinationDBName } | % {
  Get-SPSite -ContentDatabase $_ | Move-SPSite -DestinationDatabase $destinationDB -Confirm:$False
  # we can disable the content DB at the end
  Set-SPContentDatabase $_ -Status Disabled -Confirm:$False
  # or dismount it from SharePoint
  # Dismount-SPContentDatabase $_ -Confirm:$False
  # or remove it from the SharePoint server as well as from SQL server
  # Remove-SPContentDatabase $_ -Confirm:$False -Force
}

As you can see, we iterate through all site collections of all DBs in the web application (except the target content DB, ContentDB1), and move them into the ContentDB1 database using the Move-SPSite CmdLet.

At the end we can either disable the already empty content databases, dismount the from SharePoint, or remove them from the SQL server.

Don’t forget to execute IISRESET after the script to let the configuration changes be reflected in the Central Administration UI.

April 2, 2014

How to Find the Right Tool in SharePoint Central Administration Faster

Probably it’s not just because I’m a developer and not an IT-Pro, but when working with the Central Administration of SharePoint I often ask myself questions like these ones:

Where should I search the incoming e-mail configuration? In the General Application Settings? Or on the System Settings page?

As you know, the items on the Central Administration pages are stored as custom actions and custom action groups in the background. That provides a great opportunity to extend these pages via registering your own custom actions, as well makes it easy to create tools that enumerate the items on the pages, as I illustrated in this post a few years ago. I planned to create a UI extension for the Central Administration already at that time, to make it easier to find the right admin page, but until now I had not fulfilled this plan: to add a simple text input field to the start page of the Central Administration where you can type a word and an autocomplete list would display all of the matching actions.

Let’s see how to achieve that goal!

First I created a console application (SPCustomActionsExtract) that helps us to export the custom actions and groups that we will use in a JavaScript later.

In this project I defined my CustomActionGroup class that should hold the information extracted from the corresponding SPCustomActionGroupElement object. The CustomActionGroup class has the properties we need to extract, and these properties have the very same name as their counterparts in SPCustomActionGroupElement. It is important, since it makes the extraction process via Reflection very simple as we will see it later in the GetCustomActionGroup method. The class is decorated with the DataContract attribute and the properties are decorated with the DataMember attribute, since we wish to serialize the object later.

  1. [DataContract]
  2. internal class CustomActionGroup
  3. {
  4.     [DataMember]
  5.     internal string Id { get; set; }
  6.  
  7.     [DataMember]
  8.     internal string Title { get; set; }
  9.  
  10.     [DataMember]
  11.     internal string ImageUrl { get; set; }
  12.  
  13.     [DataMember]
  14.     internal List<CustomAction> CustomActions { get; set; }
  15. }

We have a similar class called CustomAction that corresponds to the SPCustomActionElement class of SharePoint. In this case, the extraction process is implemented in the GetCustomAction method (see it later).

  1. [DataContract]
  2. internal class CustomAction
  3. {
  4.     [DataMember]
  5.     internal string Title { get; set; }
  6.  
  7.     [DataMember]
  8.     internal string Description { get; set; }
  9.     
  10.     [DataMember]
  11.     internal string GroupId { get; set; }
  12.     
  13.     [DataMember]
  14.     internal string Location { get; set; }
  15.  
  16.     [DataMember]
  17.     internal string UrlAction { get; set; }
  18. }

Since we would like to use the output in JavaScript, serialization into a JSON format seems to be a good idea. I borrowed the code of the JSON serializer from this CodeProject article.

  1. internal static class JsonHelper
  2. {
  3.     public static string JsonSerializer<T>(T t)
  4.     {
  5.         DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T));
  6.         MemoryStream ms = new MemoryStream();
  7.         ser.WriteObject(ms, t);
  8.         string jsonString = Encoding.UTF8.GetString(ms.ToArray());
  9.         ms.Close();
  10.         return jsonString;
  11.     }  
  12. }

In the GetCustomActionGroups method we get all of the custom actions for a specific location. The SPCustomActionGroupElement instances are wrapped into CustomActionGroup objects in the GetCustomActionGroup method.

  1. private List<CustomActionGroup> GetCustomActionGroups(SPWeb web, String scope, String location)
  2. {
  3.     List<CustomActionGroup> customActionGroups = new List<CustomActionGroup>();
  4.  
  5.     // hack to get the Microsoft.SharPoint assembly
  6.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  7.     // and a reference to the type of the SPElementProvider internal class
  8.     Type spElementProviderType = sharePointAssembly.GetType("Microsoft.SharePoint.SPElementProvider");
  9.  
  10.     ConstructorInfo ci_SPElementProvider = spElementProviderType.GetConstructor(BindingFlags.Public | BindingFlags.Instance,
  11.          null, new Type[0], null);
  12.  
  13.     if (ci_SPElementProvider != null)
  14.     {
  15.         // spElementProvider will be of type internal class
  16.         // Microsoft.SharePoint.SPElementProvider
  17.         // defined in Microsoft.SharePoint assembly
  18.         Object spElementProvider = ci_SPElementProvider.Invoke(null);
  19.  
  20.         if (spElementProvider != null)
  21.         {
  22.             // we call
  23.             // internal List<SPCustomActionGroupElement> QueryForCustomActionGroups(SPWeb web, SPList list, string scope, string location, string groupId)
  24.  
  25.             MethodInfo mi_QueryForCustomActionGroups = spElementProviderType.GetMethod("QueryForCustomActionGroups",
  26.                     BindingFlags.NonPublic | BindingFlags.Instance, null,
  27.                     new Type[] { typeof(SPWeb), typeof(String), typeof(String) }, null
  28.                     );
  29.             if (mi_QueryForCustomActionGroups != null)
  30.             {
  31.                 // result is List<SPCustomActionGroupElement>
  32.                 IEnumerable spCustomActionGroups = (IEnumerable)mi_QueryForCustomActionGroups.Invoke(spElementProvider,
  33.                     new Object[] { web, scope, location });
  34.  
  35.                 customActionGroups = spCustomActionGroups.Cast<Object>().AsQueryable().ToList().ConvertAll(
  36.                     spCag => GetCustomActionGroup(spCag));
  37.             }
  38.         }
  39.     }
  40.  
  41.     return customActionGroups;
  42. }
  43.  
  44. private CustomActionGroup GetCustomActionGroup(object spCustomActionGroup)
  45. {
  46.     CustomActionGroup result = new CustomActionGroup();
  47.  
  48.     Type customActionGroupType = typeof(CustomActionGroup);
  49.     PropertyInfo[] cagPis = customActionGroupType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);
  50.  
  51.     // hack to get the Microsoft.SharPoint assembly
  52.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  53.     // and a reference to the type of the SPCustomActionGroupElement internal class
  54.     Type spCustomActionGroupElementType = sharePointAssembly.GetType("Microsoft.SharePoint.SPCustomActionGroupElement");
  55.  
  56.     // runtime check the type of the parameter
  57.     if (spCustomActionGroup.GetType() == spCustomActionGroupElementType)
  58.     {
  59.         List<String> propValues = new List<String>();
  60.         cagPis.Where(cagPi => cagPi.PropertyType == typeof(String)).ToList().ForEach(cagPi =>
  61.         {
  62.             string propName = cagPi.Name;
  63.             System.Reflection.PropertyInfo pi = spCustomActionGroupElementType.GetProperty(
  64.                 propName, BindingFlags.Public | BindingFlags.Instance);
  65.             if (pi != null)
  66.             {
  67.                 cagPi.SetValue(result, pi.GetValue(spCustomActionGroup, null), null);
  68.             }
  69.         });
  70.     }
  71.  
  72.     return result;
  73. }

In the GetCustomActions method we get all of the custom actions based on the location and group ID. The SPCustomActionElement instances are wrapped into CustomAction objects in the GetCustomAction method.

  1. private List<CustomAction> GetCustomActions(SPWeb web, SPList list, String scope, String location, String groupId)
  2. {
  3.     List<CustomAction> customActions = new List<CustomAction>();
  4.  
  5.     // hack to get the Microsoft.SharPoint assembly
  6.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  7.     // and a reference to the type of the SPElementProvider internal class
  8.     Type spElementProviderType = sharePointAssembly.GetType("Microsoft.SharePoint.SPElementProvider");
  9.  
  10.     ConstructorInfo ci_SPElementProvider = spElementProviderType.GetConstructor(BindingFlags.Public | BindingFlags.Instance,
  11.          null, new Type[0], null);
  12.  
  13.     if (ci_SPElementProvider != null)
  14.     {
  15.         // spElementProvider will be of type internal class
  16.         // Microsoft.SharePoint.SPElementProvider
  17.         // defined in Microsoft.SharePoint assembly
  18.         Object spElementProvider = ci_SPElementProvider.Invoke(null);
  19.  
  20.         if (spElementProvider != null)
  21.         {
  22.             // we call
  23.             // internal List<SPCustomActionElement> QueryForCustomActions(SPWeb web, SPList list, string scope, string location, string groupId)
  24.  
  25.             MethodInfo mi_QueryForCustomActions = spElementProviderType.GetMethod("QueryForCustomActions",
  26.                     BindingFlags.NonPublic | BindingFlags.Instance, null,
  27.                     new Type[] { typeof(SPWeb), typeof(SPList), typeof(String), typeof(String), typeof(String) }, null
  28.                     );
  29.             if (mi_QueryForCustomActions != null)
  30.             {
  31.                 // result is List<SPCustomActionElement>
  32.                 IEnumerable spCustomActions = (IEnumerable)mi_QueryForCustomActions.Invoke(spElementProvider,
  33.                     new Object[] { web, list, scope, location, groupId });
  34.  
  35.                 customActions = spCustomActions.Cast<Object>().AsQueryable().ToList()
  36.                                                                   .ConvertAll(spCa => GetCustomAction(spCa));
  37.             }
  38.         }
  39.     }
  40.  
  41.     return customActions;
  42. }
  43.  
  44. private CustomAction GetCustomAction(object spCustomAction)
  45. {
  46.     CustomAction result = new CustomAction();
  47.     
  48.     Type customActionType = typeof(CustomAction);
  49.     PropertyInfo[] caPis = customActionType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);
  50.  
  51.     // hack to get the Microsoft.SharPoint assembly
  52.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  53.     // and a reference to the type of the SPCustomActionElement internal class
  54.     Type spCustomActionElementType = sharePointAssembly.GetType("Microsoft.SharePoint.SPCustomActionElement");
  55.  
  56.     // runtime check the type of the parameter
  57.     if (spCustomAction.GetType() == spCustomActionElementType)
  58.     {
  59.         List<String> propValues = new List<String>();
  60.         caPis.ToList().ForEach(caPi =>
  61.         {
  62.             string propName = caPi.Name;
  63.             System.Reflection.PropertyInfo pi = spCustomActionElementType.GetProperty(
  64.                 propName, BindingFlags.Public | BindingFlags.Instance);
  65.             if (pi != null)
  66.             {
  67.                 caPi.SetValue(result, pi.GetValue(spCustomAction, null), null);
  68.             }
  69.         });
  70.     }
  71.  
  72.     return result;
  73. }

The possible values of the locations for the Central Administration are defined in the locations list. You find these values on MSDN. In this case we ignored the start page (location "Microsoft.SharePoint.Administration.Default"), as it contains only duplications of custom actions from other pages.

In the ExtractCustomActions method we iterate through all of these locations, get the custom action groups, and each custom actions in the groups, accumulate them into the allCustomActionGroups list, and finally save the JSON serialized result into a file.

  1. // Central Administration Custom Action Locations
  2. //http://msdn.microsoft.com/en-us/library/bb802730.aspx
  3. List<String> locations = new List<String>() {
  4.                         "Microsoft.SharePoint.Administration.Applications",
  5.                         "Microsoft.SharePoint.Administration.Backups",
  6.                         "Microsoft.SharePoint.Administration.ConfigurationWizards",
  7.                         // we don't need duplicates, so we eliminate this one
  8.                         //"Microsoft.SharePoint.Administration.Default",
  9.                         "Microsoft.SharePoint.Administration.GeneralApplicationSettings",
  10.                         "Microsoft.SharePoint.Administration.Monitoring",
  11.                         "Microsoft.SharePoint.Administration.Security",
  12.                         "Microsoft.SharePoint.Administration.SystemSettings",
  13.                         "Microsoft.SharePoint.Administration.UpgradeAndMigration"
  14.                      };
  15.  
  16.  
  17. private void ExtractCustomActions()
  18. {
  19.     List<CustomActionGroup> allCustomActionGroups = new List<CustomActionGroup>();
  20.  
  21.     // get the site collection of the Central Administration web application
  22.     SPAdministrationWebApplication centralAdmin = SPAdministrationWebApplication.Local;
  23.     using (SPSite site = centralAdmin.Sites[0])
  24.     {
  25.         using (SPWeb web = site.OpenWeb())
  26.         {
  27.             locations.ForEach(location =>
  28.                 {
  29.                     List<CustomActionGroup> customActionGroup = GetCustomActionGroups(web, null, location);
  30.                     allCustomActionGroups.AddRange(customActionGroup);
  31.                     customActionGroup.ForEach(cag =>
  32.                         {
  33.                             cag.CustomActions = GetCustomActions(web, null, null, location, cag.Id);
  34.                         });
  35.                 });
  36.         }
  37.     }
  38.  
  39.     string customActions = JsonHelper.JsonSerializer(allCustomActionGroups);
  40.     File.WriteAllText("CustomActions.json", customActions);
  41. }

We should insert the following chunk of HTML code into the start page of the Central Administration, to achieve that I inserted a Content Editor Web Part (CEWP) to the right web part zone of the page. As you can see I utilized jQuery, the LINQ for JavaScript (ver.3.0.3-Beta4) library and jQuery UI autocomplete. The single HTML element is a text field to which we can attach the autocomplete behavior.

  1. <!– jQuery –>
  2. <script type="text/javascript" src="/_layouts/CACustomActions/js/jQuery/jquery-1.8.3.min.js"></script>
  3. <!– LINQ.js –>
  4. <script src="/_layouts/CACustomActions/js/linq.min.js" type="text/javascript"></script>
  5. <!– jQuery UI autocomplete –>
  6. <script type="text/javascript" src="/_layouts/CACustomActions/js/jquery-ui-1.10.3.custom/js/jquery-ui-1.10.3.custom.min.js"></script>
  7. <link rel="stylesheet" type="text/css" href="/_layouts/CACustomActions/js/jquery-ui-1.10.3.custom/css/ui-lightness/jquery-ui-1.10.3.custom.min.css">
  8. <!– Our custom .js / .css components –>
  9. <script src="/_layouts/CACustomActions/js/CustomActions.js" type="text/javascript"></script>
  10. <link rel="stylesheet" type="text/css" href="/_layouts/CACustomActions/css/CustomActions.css">
  11.  
  12. <input id="autocompleteCustomActions" type="text"/>

In the CustomActions.js I have a variable called CustomActions.js that contains the JSON serialized output of the custom actions and groups from our SPCustomActionsExtract tool.

On page load we register an event handler that invokes the updateAutoComplete method whenever we type a text into the text field.

  1. $(document).ready(startScript);              
  2.         
  3. function startScript() {
  4.   registerEvents();
  5. }
  6.  
  7. function registerEvents() {     
  8.        $("#autocompleteCustomActions").keyup(function(e){
  9.        updateAutoComplete();
  10.     });
  11. }

In the first part of the updateAutoComplete method we compare the titles and descriptions of the existing custom actions to the filter value we typed in, and aggregating the result into the matchingCAs array. Next, the items are ordered alphabetically

  1. var searchedCA = $("#autocompleteCustomActions").val().toLowerCase();
  2.  
  3. var matchingCAs = new Array();
  4.  
  5. Enumerable.from(customActions).forEach(function (cag) {
  6.   Enumerable.from(cag.CustomActions).forEach(function (ca) {
  7.     // find the custom action based on the title and the description
  8.     // the comparision is case insensitive
  9.     if ((ca.Title.toLowerCase().indexOf(searchedCA) > -1) || ((ca.Description != undefined) && (ca.Description.toLowerCase().indexOf(searchedCA) > -1))) {
  10.       var desc = (ca.Description != undefined) ? ca.Description  : ""
  11.       matchingCAs.push({
  12.           // HACK?: we have to create a 'label' property that inlcudes the 'Title' and the 'Description'
  13.           // otherwise the item is not shown in the autocomplete list
  14.           // I don't know if it is a feature or a bug
  15.           label: ca.Title + " – " + desc,
  16.           caption: ca.Title,
  17.           description: desc,
  18.           groupTitle: cag.Title,
  19.           urlAction: ca.UrlAction,
  20.           imageUrl: cag.ImageUrl
  21.       });
  22.     }
  23.   });
  24. });
  25.  
  26. matchingCAs = Enumerable.from(matchingCAs).orderBy("$.label").toArray();

Then we display the result in the autocomplete box. We display the icon of the custom action group and the title of the custom action in the list, but as a tool tip the description of the custom action and the title of the group will be display as well. The URL of the custom action is set as a link on the item, so if you select an item, the corresponding page will be opened in the browser.

  1. $("#autocompleteCustomActions").autocomplete({
  2.     sortResults:true,
  3.     source: matchingCAs,
  4.     open: function() {
  5.         $('#autocompleteCustomActions').autocomplete('widget').width(350);
  6.     },
  7.     focus: function(event, ui) {
  8.         $('#autocompleteCustomActions').val(ui.item.label);
  9.         return false;
  10.     }
  11. })
  12. .data("ui-autocomplete")._renderItem = function(ul, item) {
  13.     return $("<li>")
  14.     .append("<a class='ca-text' href='" + item.urlAction + "' title='" + item.description + " (" + item.groupTitle + ")'><img class='ca-img' src='"+ item.imageUrl + "' /><span class='ca-wrapper'>" + item.caption + "</span></a>")
  15.     .appendTo(ul);
  16. };

The screenshots below illustrate the functionality of the sample. As we type the text, the autocomplete list changes dynamically:

image

If you hover over a list item, additionally information is displayed as tool tip, so we can easily decide if it is really the administrative option we need. If you click on an item in the list, the corresponding page will be opened.

image

You can download the sample application (the .json generator tool, web part, .js and .css files) from here.

The deployment steps:

Copy the content of the Layouts folder from the sample into the LAYOUTS folder (C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS) of your SharePoint server.

If your Central Administration contains special custom actions, you can (and should) update the value of the customActions variable at the very beginning of the CustomActions.js file (in the CACustomActions\js folder in LAYOUTS) with the content of the CustomActions.json generated by the SPCustomActionsExtract.exe tool. You should find the .json file in the bin\Debug folder of the project. The .json file I used (and that is included in the sample) was generated from a standard installation of the English version of SharePoint Server 2010 Enterprise edition, so if you have an other version (like Foundation or a German version) you should definitely regenerate the file and update the CustomActions.js file with the result.

Note: If you don’t like the idea of using a command line tool to (re)generate the structure queried from JavaScript, it is viable option to create a custom web part that performs the same actions on-demand (that means, on each page request) as this tool does, and includes the client side components (like JavaScript and css) as well. However, I found that since the registered custom actions do not change very frequently, so creating and deploying a web part seemed me a bit overkill in this case.

The web part (see WebPart\Find the Page You Need.dwp) can be imported to the start page of the the Central Administration, or if you find that easier, you can add a Content Editor Web Part (CEWP) to the page and set its source code based on the code snippet in this post above.

May 13, 2011

‘User cannot be found’ error solved using the SharePoint object model

Filed under: Administration, Bugs, Reflection, SP 2010 — Tags: , , , — Peter Holpar @ 21:24

In my recent post I wrote about the ‘User cannot be found‘ error on the Change site collection administrators page and how to find the source of the issue using a SQL query.

In the current post I would like to provide you a bit more supported and more automated solution using the server side SharePoint API and some Reflectioning.

In this approach we first gets the list of all admin and content web applications, then iterate through the site collection of each web apps.

  1. private void CheckSiteAdminsOnFarm()
  2. {
  3.  
  4.     // we check both admin and content sites
  5.     List<SPWebApplication> appsToCheck = SPWebService.AdministrationService.WebApplications.Union(
  6.         SPWebService.ContentService.WebApplications).ToList();
  7.  
  8.     // we iterate through all site collections of all apps
  9.     appsToCheck.ForEach(webApp => webApp.Sites.ToList().ForEach(site => CheckSiteAdmins(site, @"SPMMX\administrator")));
  10.  
  11. }

You can see that we provide a login name to the CheckSiteAdmins method. Admin users that cannot be resolved as an existing user will be replaced with this user account.

In CheckSiteAdmins we first have to initialize the site using the private InitSite method of the SPSite class. It is necessary to call  InitSite, since it populates the value of the private m_OwnerID and m_nSecondaryContactID fields. Next we read the int values of these fields and try to resolve the to user through our GetUserName method.

  1. private void CheckSiteAdmins(SPSite site, String adminUser)
  2. {
  3.     try
  4.     {
  5.         Console.WriteLine("Checking site: '{0}' ({1})", site.RootWeb.Title, site.Url);
  6.  
  7.         Type spSiteType = typeof(SPSite);
  8.  
  9.         // site must be initialized before accessing the contact info
  10.         MethodInfo mi_InitSite = spSiteType.GetMethod("InitSite",
  11.                         BindingFlags.NonPublic | BindingFlags.Instance, null,
  12.                         new Type[0], null);
  13.  
  14.         if (mi_InitSite != null)
  15.         {
  16.             mi_InitSite.Invoke(site, null);
  17.  
  18.             // get field info of m_OwnerID
  19.             FieldInfo fi_m_OwnerID = spSiteType.GetField("m_OwnerID",
  20.                     BindingFlags.NonPublic | BindingFlags.Instance);
  21.             // get field info of m_nSecondaryContactID
  22.             FieldInfo fi_m_nSecondaryContactID = spSiteType.GetField("m_nSecondaryContactID",
  23.                     BindingFlags.NonPublic | BindingFlags.Instance);
  24.  
  25.             String currentAdmin = String.Empty;
  26.             bool isFound = false;
  27.  
  28.             if (fi_m_OwnerID != null)
  29.             {
  30.                 int primaryContactId = (int)fi_m_OwnerID.GetValue(site);
  31.                 isFound = GetUserName(site, primaryContactId, out currentAdmin);
  32.                 Console.WriteLine("Primary: {0}; {1}", primaryContactId, currentAdmin);
  33.                 if (!isFound)
  34.                 {
  35.                     Console.WriteLine("Fix site owner to '{0}'", adminUser);
  36.                     site.Owner = site.RootWeb.EnsureUser(adminUser);
  37.                 }
  38.             }
  39.  
  40.             if (fi_m_nSecondaryContactID != null)
  41.             {
  42.                 int secondaryContactId = (int)fi_m_nSecondaryContactID.GetValue(site);
  43.                 isFound = GetUserName(site, secondaryContactId, out currentAdmin);
  44.                 Console.WriteLine("Secondary: {0}; {1}", secondaryContactId, currentAdmin);
  45.                 if (!isFound)
  46.                 {
  47.                     Console.WriteLine("Fix secondary contact to '{0}'", adminUser);
  48.                     site.SecondaryContact = site.RootWeb.EnsureUser(adminUser);
  49.                 }
  50.             }
  51.         }
  52.     }
  53.     catch (Exception ex)
  54.     {
  55.         Console.WriteLine(ex.Message);
  56.     }
  57. }

If the user set as the admin is invalid, we simply replace it with the default value passed in the adminUser parameter.

Side note: In the former post I wrote a few words about the internal GetByIDNoThrow method and the private FindUserNoThrow method of the SPUserCollection class. These method may help to create alternative ways to lookup the user, but it is important to let the caller method know, that we found no user because none was set or because the one that was set cannot be resolved. So passing back an SPUser instance with a simple null value is not a solution.

Our implementation of GetUserName method looks like illustrated by the following code block:

  1. private bool GetUserName(SPSite site, int userId, out String currentAdmin)
  2. {
  3.     currentAdmin = "Not specified";
  4.     bool isFound = true;
  5.  
  6.     if (userId != 0)
  7.     {
  8.         SPUser user = site.RootWeb.SiteUsers.Cast<SPUser>().AsQueryable().FirstOrDefault(usr => usr.ID == userId);
  9.         currentAdmin = (user == null) ? "Unknown user" : user.Name;
  10.         isFound = (user != null);
  11.     }
  12.  
  13.     return isFound;
  14. }

Running the code as part of a console application via calling the CheckSiteAdminsOnFarm method displays the current site owners and secondary contacts of all the sites of all the web applications, and replaces the invalid values if necessary.

‘User cannot be found’ error solved using a SQL query

Filed under: Administration, Bugs, Content database, SP 2010 — Tags: , , , — Peter Holpar @ 21:23

A few days ago I created a new web application on one of our servers, and just was to check back if I set the right user as the site owner, but clicking the Change site collection administrators link in central admin displayed the following error message:
 image

In the SharePoint log I found the corresponding lines:

05/12/2011 12:21:24.94     w3wp.exe (0x19DC)                           0x12A0    SharePoint Foundation             Logging Correlation Data          xmnv    Medium      Name=Request (GET:http://adminsite/_admin/owners.aspx)    01740b25-3356-433e-85af-49d6c90bd9bb
05/12/2011 12:21:24.94     w3wp.exe (0x19DC)                           0x12A0    SharePoint Foundation             Logging Correlation Data          xmnv    Medium      Site=/    01740b25-3356-433e-85af-49d6c90bd9bb
05/12/2011 12:21:24.95     w3wp.exe (0x19DC)                           0x12A0    SharePoint Foundation             Database                          4ohp    High        Enumerating all sites in SPWebApplication Name=Customerext.    01740b25-3356-433e-85af-49d6c90bd9bb
05/12/2011 12:21:24.95     w3wp.exe (0x19DC)                           0x12A0    SharePoint Foundation             Database                          4ohq    Medium      Site Enumeration Stack:    at Microsoft.SharePoint.SPBaseCollection.GetEnumerator()     at Microsoft.SharePoint.WebControls.SiteAdministrationSelector.DefaultSelectionId()     at Microsoft.SharePoint.WebControls.ContextSelector`1.OnLoad(EventArgs e)     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBefore…    01740b25-3356-433e-85af-49d6c90bd9bb
05/12/2011 12:21:24.95*    w3wp.exe (0x19DC)                           0x12A0    SharePoint Foundation             Database                          4ohq    Medium      …AsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.ProcessRequest()     at System.Web.UI.Page.ProcessRequest(HttpContext context)     at ASP._admin_owners_aspx.ProcessRequest(HttpContext context)     at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)     at System.Web.HttpApplication.PipelineStepManager.ResumeSteps(Exception error)     at System.Web.HttpApplication.BeginProcessRequestNotification(HttpContext context, AsyncCallback cb)     at System.Web.HttpRuntime.ProcessRequestNotificationPrivate(IIS7WorkerRequest wr, HttpContext context)     at System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelpe…    01740b25-3356-433e-85af-49d6c90bd9bb
05/12/2011 12:21:24.95*    w3wp.exe (0x19DC)                           0x12A0    SharePoint Foundation             Database                          4ohq    Medium      …r(IntPtr managedHttpContext, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)     at System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr managedHttpContext, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)     at System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr managedHttpContext, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)     at System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr managedHttpContext, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)      01740b25-3356-433e-85af-49d6c90bd9bb
05/12/2011 12:21:24.97     w3wp.exe (0x19DC)                           0x12A0    SharePoint Foundation             General                           72nz    Medium      Videntityinfo::isFreshToken reported failure.    01740b25-3356-433e-85af-49d6c90bd9bb
05/12/2011 12:21:24.98     w3wp.exe (0x19DC)                           0x12A0    SharePoint Foundation             Runtime                           tkau    Unexpected    Microsoft.SharePoint.SPException: User cannot be found.    at Microsoft.SharePoint.SPUserCollection.GetByID(Int32 id)     at Microsoft.SharePoint.Administration.SPSiteAdministration.get_SecondaryContactLoginName()     at Microsoft.SharePoint.ApplicationPages.OwnersPage.OnContextChange(Object sender, EventArgs e)     at Microsoft.SharePoint.WebControls.ContextSelector`1.OnContextChange(EventArgs e)     at Microsoft.SharePoint.WebControls.ContextSelector`1.set_CurrentId(String value)     at Microsoft.SharePoint.WebControls.ContextSelector`1.OnLoad(EventArgs e)     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at Sys…    01740b25-3356-433e-85af-49d6c90bd9bb
05/12/2011 12:21:24.98*    w3wp.exe (0x19DC)                           0x12A0    SharePoint Foundation             Runtime                           tkau    Unexpected    …tem.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)    01740b25-3356-433e-85af-49d6c90bd9bb
05/12/2011 12:21:24.98     w3wp.exe (0x19DC)                           0x12A0    SharePoint Foundation             Monitoring                        b4ly    Medium      Leaving Monitored Scope (Request (GET:http://adminsite/_admin/owners.aspx)). Execution Time=54.1751647921174    01740b25-3356-433e-85af-49d6c90bd9bb

It suggested that there should be a non-existing user set as the secondary admin of the site. For example, a user was set as the (secondary) site admin and that user was deleted later on, before being replaced on that role.

I’ve discussed the issue with the site operators, and they told me that most of the sites on the server were migrated from a MOSS 2007 server, and probably it caused the mismatch of the user IDs.

I found the following (MOSS 2007) resources on the web when looking for a confirmation of my theory:

Error message when you try to manage a site collection in Windows SharePoint Services 3.0 or in SharePoint Server 2007: "User cannot be found"

User cannot be found error

I’ve checked the code that caused the issue and found that getters of both the Owner and SecondaryContact properties of the SPSite class contains a call like this:

this.RootWeb.SiteUsers.GetByID(this.m_OwnerID)

Of course, for the SecondaryContact  the m_nSecondaryContactID field is passed as parameter.

The GetByID method of the SPUserCollection class throws an exception if the specified user is not found in the collection.

Side note: GetByID method calls the internal GetByIDNoThrow method that does not throw any exception if the user specified by the ID is not found. This latter method calls forward to the private FindUserNoThrow method that is able to find the position of a user in the collection based on a property specified by the nested internal enum UserFields and an explicit value to compare against. This method has two overloads, one for String-based, and another one for int-based properties. The method returns the result of the search as a bool value, and the position of the first match as an out uint parameter. Using this value, one can get the corresponding SPUser object through the this[int index] indexer of SPUserCollection.

Possible SPUser properties to search for in the SPUserCollection using the FindUserNoThrow method and the UserFields enum:

ID, Sid, Name, LoginName, Email, Notes, IsSiteAdmin, IsDomainGroup, Flags, Max

But let’s back to the topic. I consider this behavior of the site collection admin page to be a bug, and would expect this internal exception to be handled and displayed for end users on the UI to enable them to resolve the issue there.

Since there are a lot of web applications with several site collections on the server, it was the first and probably the hardest part to identify the ones that have incorrect site collection admin settings. In this post I show you a way that might be not supported, but for an administrator probably the quickest one, and that is a query against the SharePoint configuration and content databases.

In the query (that assumes the configuration database is called SharePoint_Config) we first create a SQL cursor and looks up all the content databases, then queries that content DBs one by one to check the site collection settings and existing users. In the query we check both for physically deleted user records, and logically deleted ones (where the UserInfo record exist, but its tp_Deleted field is set to 1).

The query results are aggregated into a temp table, that is display at the end of the query, and is finally dropped.

  1. USE SharePoint_Config
  2.  
  3. CREATE TABLE ##SiteAdmins (
  4. DBName NVARCHAR(200),
  5. SiteId UNIQUEIDENTIFIER,
  6. SiteInfo NVARCHAR(200),
  7. OwnerID INT,
  8. PrimaryAdmin NVARCHAR(255),
  9. PrimaryAdminDeleted INT,
  10. SecondaryContactID INT,
  11. SecondaryAdmin NVARCHAR(255),
  12. SecondaryAdminDeleted INT,
  13. )
  14.  
  15. DECLARE @contentDbName NVARCHAR(200);
  16.  
  17. DECLARE contentDbCursor CURSOR FOR
  18. SELECT [Name] FROM [Objects]
  19. WHERE Properties LIKE '<object type="Microsoft.SharePoint.Administration.SPContentDatabase, Microsoft.SharePoint%'
  20.  
  21. OPEN contentDbCursor
  22.  
  23. FETCH NEXT FROM contentDbCursor
  24. INTO @contentDbName
  25.  
  26. WHILE @@FETCH_STATUS = 0
  27. BEGIN
  28.  
  29. DECLARE @cmd AS VARCHAR(2000)
  30.  
  31. SET @cmd = 'USE [' + @contentDbName + ']
  32. INSERT INTO ##SiteAdmins
  33. (DBName, SiteId, SiteInfo, OwnerID, PrimaryAdmin, PrimaryAdminDeleted,  SecondaryContactID, SecondaryAdmin, SecondaryAdminDeleted)
  34. SELECT
  35.   ''' + @contentDbName + ''' AS DBName,
  36.   S.Id AS SiteId,
  37.   ''/'' + W.FullUrl + '' ('' + W.Title + '')'' AS SiteInfo,
  38.   S.OwnerID,
  39.   CASE
  40.       WHEN S.OwnerID IS NULL THEN ''Not specified''
  41.       ELSE ISNULL(PA.tp_Title, ''Unknown user'')
  42.   END AS PrimaryAdmin,
  43.   CASE WHEN
  44.     (S.OwnerID IS NOT NULL AND PA.tp_Title IS NULL)
  45.     OR PA.tp_Deleted = 1 THEN 1
  46.     ELSE 0
  47.   END AS PrimaryAdminDeleted,
  48.   S.SecondaryContactID,  
  49.   CASE
  50.       WHEN S.SecondaryContactID IS NULL THEN ''Not specified''
  51.       ELSE ISNULL(SA.tp_Title, ''Unknown user'')
  52.   END AS SecondaryAdmin,
  53.   CASE WHEN
  54.     (S.SecondaryContactID IS NOT NULL AND SA.tp_Title IS NULL)
  55.     OR SA.tp_Deleted = 1 THEN 1
  56.     ELSE 0
  57.   END AS SecondaryAdminDeleted
  58. FROM Sites S
  59. LEFT JOIN UserInfo PA ON PA.tp_ID = S.OwnerID AND PA.tp_SiteID = S.Id
  60. LEFT JOIN UserInfo SA ON SA.tp_ID = S.SecondaryContactID AND SA.tp_SiteID = S.Id
  61. LEFT JOIN Webs W ON W.Id = S.RootWebId AND W.SiteId = S.Id'
  62.  
  63. EXECUTE(@cmd)
  64.  
  65. FETCH NEXT FROM contentDbCursor
  66.     INTO @contentDbName
  67. END
  68.  
  69. CLOSE contentDbCursor
  70. DEALLOCATE contentDbCursor
  71.  
  72. SELECT DBName, SiteId, SiteInfo, OwnerID, PrimaryAdmin, PrimaryAdminDeleted, SecondaryContactID, SecondaryAdmin, SecondaryAdminDeleted FROM ##SiteAdmins
  73.  
  74. DROP TABLE ##SiteAdmins

If there is a primary or secondary admin that is displayed as deleted by the script, it must be handled to correct the ‘User cannot be found’ error.

Although you can correct these entries directly in the content DB, either manually or even by extending the above script to update the OwnerID or SecondaryContactID field to a predefined value (like 1 that means the first user in the site or null to no user specified), it is strongly recommended to use a supported tool at least for the data modification part. Such tool might be the STSADM command:

stsadm.exe -o siteowner -url Url -ownerlogin DOMAIN\name -secondarylogin DOMAIN\name

or you can set the Owner and SecondaryContact property of the erroneous SPSite from PowerShell or C#.

You can read a full managed solution in my next post.

February 11, 2010

A tip about configuring Service Applications

Filed under: Administration, SP 2010, Tips & Tricks — Tags: , , — Peter Holpar @ 12:38

If you create a new User Profile Service instance (Application Management / Service Applications / Manage service applications), but forgot to start the User Profile Service (System Settings / Servers / Manage services on server) before creating the service instance you may receive the following warning message when you try to configure the newly created instance:

This User Profile Application’s connection is currently not available. The Application Pool or User Profile Service may not have been started. Please contact your administrator.

This message is similar to one you receive when creating a Managed Metadata Service instance but forgot to start the Managed Metadata Web Service:

The Managed Metadata Service or Connection is currently not available. The Application Pool or Managed Metadata Web Service may not have been started. Please Contact your Administrator.

But I’ve found a significant difference in the resolution. While it is enough to simply start the Managed Metadata Web Service, you should run IISRESET in the case of User Profile Service.

Create a free website or blog at WordPress.com.