Brett's SharePoint Blog

  • Master Pages Revisited: References to master pages in SharePoint

    I'm still neck-deep in WCM planning, design, and development on my current project and also get lots of questions on branding topics, so a frequent point of discussion is various design patterns for the use of master pages. One of the topics that's come up a lot recently is "how do I assign master page X to page (or page layout) Y", so I've attempted to summarize my findings and recommendations on this topic in a handy table in this post.

    First off, a refresher on key jargon used in the table...

    Page Types - these include System Pages (i.e. web pages & web part pages that aren't based on page layouts and list view/display form pages), Page Layouts, and Application Pages.

    There are five techniques described in the table for assigning a master page reference:

    • Hardcoded
      This is a relative or absolute path to a master page. Relative locations are based on the page instance's location - i.e. "../_catalogs/masterpage/this.master" will find this.master in the current SPWeb's master page gallery, which may or may not be the top-level site's master page gallery.
    • Token-based References as described in this article 
      • Static Token for Site-Relative Master Page
        Note that the syntax for using this token is incorrect in the article - the correct syntax is ~site/_catalogs/masterpage/yourmasterpage.master.
      • Static Token for Site Collection-Relative Master Page
        Note that the syntax for using this token is incorrect in the article - the correct syntax is ~sitecollection/_catalogs/masterpage/yourmasterpage.master.
      • Dynamic Token for SystemMaster Page
        The ~masterurl/default.master token will use the current site's System Master Page.
      • Dynamic Token for Site Master Page
        The ~masterurl/custom.master token will use the current site's Site Master Page.

     

    Approach

    Page Type

    Result

    Comments

    Hardcode

    ../_catalogs/masterpage/XXX.master

    (or ../../_catalogs, etc., depending on how deep the page is located in the site hierarchy)

    System

    Works - desired master page is used

     

    ~masterurl/default.master

    System

    Works - System Master Page is used

     

    ~masterurl/custom.master

    System

    Works - Site Master Page is used

     

    ~site/_catalogs/masterpage/XXX.master

    System

    Works - desired master page is used

     

    ~sitecollection/_catalogs/masterpage/XXX.master

    System

    Works - desired master page is used

     

    Hardcode

    ../_catalogs/masterpage/XXX.master

    Page Layout

    Error

    Even if this did work, hardcoding a reference to a master page with this kind of relative path is a bad idea - remember that you may have subsites!

    ~masterurl/default.master

    Page Layout

    No effect

    Simplest approach is based on information architecture - if a publishing page needs to use a different master page, put it in a different site.

     

    If this doesn't cut it, ask yourself why you really need two different looks & feels for a single site. Remember that you can override just about anything from the master page in the page layout. Did you think about designing your master page in such a way that you can override the undesirable  behavior/appearance in the page layout?

    ~masterurl/custom.master

    Page Layout

    No effect

    Same as above.

    ~site/_catalogs/masterpage/XXX.master

    Page Layout

    No effect

    Same as above.

    ~sitecollection/_catalogs/masterpage/XXX.master

    Page Layout

    No effect

    Same as above.

    Hardcode

    ../catalogs/masterpage/XXX.master

    Application

    Error

    Even if this did work, hardcoding a reference to a master page with this kind of relative path is a bad idea - remember that you may have subsites!

    ~masterurl/default.master

    Application

    Error

    See my first big branding post for techniques applying to adjusting the look & feel of LAYOUTS pages - http://blogs.msdn.com/bgeoffro/archive/2007/11/12/branding-a-moss-corporate-intranet-portal-part-1b-branding-methods-by-scope-and-scenario.aspx .

    If you want to use a site or site collection-specific master page, the solution must involve an HttpModule. It'd be a different kind of branding module than I've described in my previous branding posts, as the module would need to read the Site Master Page or (more likely) the System Master Page from the current SPWeb based on the path, and then apply it to the page.

    ~masterurl/custom.master

    Application

    Error

    Same as above.

    ~site/_catalogs/masterpage/XXX.master

    Application

    Error

    Same as above.

    ~sitecollection/_catalogs/masterpage/XXX.master

    Application

    Error

    Same as above.

    Note that I didn't mention Site Pages (pages based on page layouts) anywhere in the table. You can't assign a master page to them using any of these techniques since they inherit from a page layout. Your only option in the unusual scenario of assigning a specific master page to a specific Site page would be to use a HttpModule that references the absolute path to that specific page and reassigns the master page. A better approach would be to use a page layout specifically designed for that page instance, and then override the master page as needed in the page layout.

     

  • Adventures in Regional Settings with Content Query Web Parts & RSS Viewer Web Parts

    Here's another interesting one I encountered with multilanguage WCM sites while living here in Europe. Seems simple enough, but as always with multilanguage sites, appearances can be deceiving...

    The Requirement

    This one's pretty basic. We've got a couple of big lines of business for my customer, and they each have large intranet portals that use CQWPs to aggregate intra-site content and RSS Viewers to aggregate content from outside the site collection. In the course of building their portals, the site owners came up with the fairly common requirement of developing site collection-specific styles for presenting content in these CQWPs and RSS Viewers - special formatting for titles, descriptions/content, and authors, images, etc. Rules of engagement for this environment forbade customizations without a strong business case, so we were limited to OOTB web parts, browser-based customizations, and SPD-based customizations. 

    The Initial Solution

    The RSS Viewer solution was a simple design pattern. Develop the custom XSL, embed it in the .webpart configuration info, and install the .webpart in the Web Part Gallery for the site.

    The CQWP solution was only slightly more complicated. Develop the custom XSL, install it in the Style Library, update the .webpart configuration info to reference the custom style sheet instead of ItemStyle.xsl, and install the .webpart in the Web Part Gallery for the site.

    The Complication

    The site collections involved here had sites in more than one language in each site collection, as they were using variations. This means that the formatting of dates in the RSS Web Parts and CQWPs could be an issue - using a static date format as per the customer's original specifications would be a problem. For example, sites with the English-US region would use the M/D/YYYY date format, while sites with the Dutch-NL region would use D-M-YYYY format for all standard fields. If we hardcoded one of these formats in the RSS Viewers or CQWPs, we could potentially have dates appearing in multiple formats on any given page. Ugly.

    Fortunately, there's a handy XSL function in the ddwrt namespace called "FormatDate" for addressing this issue - it takes a string, LCID, and format type as arguments to adjust the date format, as described here. Unfortunately, there are two major problems in getting the right LCID unless you can extend these webparts:

    • The RSS Viewer Web Part has a hardcoded parameter in its XSL that sets the LCID to 1033. It has nothing to do with the Regional Settings for the site.
    • With CQWPs, there is no LCID parameter at all that is accessible to the XSL.

    What to do?

    The Solution (Browser/SPD Customizations Only)

    For the CQWPs, we're fortunate enough to be able to select from the multiple styles at our disposal via the XSL file in the Styles Library. For each item style that includes a date, we can simply create a separate template for each language - the same template, with a different LCID argument and a different name. For example, we could have "Date and Title (Dutch-NL)" and "Date and Title (English-US)" as separate item styles. Train the users to select the appropriate style for that site. Slightly ugly implementation, but user-friendly if you have a limited number of languages, and it sticks to the rules of keeping the customizations within the site collection. Solution architecture remains unchanged - we just have slighly more elaborate XSL.

    For the RSS Viewer, things get uglier. The only way of selecting the XSL is by specifying a URL to an external XSL file in the "XSL Link" field. So you have one of two options:

    • Store a version of the RSS XSL for each language and train users to select the right one
    • Store a version of the RSS Viewer for each language and train users to select the right one. I prefer this option since it's simpler from a training perspective.

    The Real Solution

    Both of the workarounds above are only practical if you have a very small number of languages and can absorb the training costs for a small pool of content authors. For an enterprise-level deployment of this kind of functionality, you need to implement custom versions of the CQWP and RSS Viewer that can pull the LCID out of the SPWeb.RegionalSettings and pass it to the XSLT (or find third-party versions that do the same).

  • Using custom scopes on multilanguage variation sites

    One of the great things about working on MOSS projects here in Europe is the prevalance of multilanguage requirements. You just don't encounter multilanguage requirements (at least on intranets & extranets) nearly as often in the US. I've had a lot of deep, hands-on experience with custom and variations-based multilanguage sites this year, and wanted to provide a heads-up regarding a problem and a novel solution when using variations and search.

    The Problem

    This is a pretty straightforward scenario you'd encounter if you are using variations to implement a multilanguage site. Assuming you've got the default shared scopes configured and are using a master page like default.master that uses the SmallSearchInputBox delegate control, here's the repro:

    1. Create a new site collection in language X (English in this example).
    2. Configure the site collection to use custom scopes (Site Actions > Modify All Site Settings > Site Settings > Search Settings > Use Custom Scopes).
    3. Set the variation home ("/" in this example).
    4. Create a variation label for language X (English in this example)
    5. Create a variation label for language Y (Dutch in this example)
    6. Create the variation hierarchies
    7. Navigate to the welcome page for variation site in language X
    8. Select the search dropdown control - "This Site:...", "All Sites", "People" appear in the list (these labels are different if language X is not English).
    9. Navigate to the welcome page for variation site in language Y
    10. Select the search dropdown control - "Deze Site:..." appears in the list (this label is different if language Y is not Dutch).

    You'll find "All Sites" and "People" (and any other shared scopes) are missing for any variation site where the language is different from the root site. So where did they go?

    The key question is, "why do items appear in the search dropdown control?" The dependency in question is a Search Display Group (Site Actions > Modify All Site Settings > Site Settings > Search Scopes). In single-language site collections, display groups and search scopes all use the same language, so no special configuration is necessary.

    In Multilanguage site collections – such as those that use variations – there is an important difference in behaviors. As with single-language site collections, the search scopes and display groups are created in the top-level site’s language. However, some of the subsites are created in different languages. On each site, the search control will look for the scopes in the Search Dropdown display group (which is named differently for each language) to determine which scopes to show on the search control.

    On the sites where the language is the SAME as the top-level site, all the scopes in the Search Dropdown display group will render – this is because the names of the Search Dropdown display group in the search settings and the search control match. For example, if the top-level site is English, the Search Dropdown display group is named “Search Dropdown”, and the search control on English sites will be looking for scopes in a display group named “Search Dropdown”.

    On the sites where the language is DIFFERENT from the top level site, none of the scopes in the Search Dropdown display group will render – this is because the names of the Search Dropdown display group in the search settings and the search control do not match. For example, if the top-level site is English, the Search Dropdown display group is named “Search Dropdown”, and the search control on Dutch sites will be looking for scopes in a display group named “Vervolgkeuzelijst voor zoeken”, which does not exist.

    The Design

    Our solution was based on our requirements, which is to support the set of five languages required by our customer. The solution components are a single site collection-scoped feature with a feature receiver, which are manually activated as needed by site collection owners for sites that use variations. When the feature is activated, the feature receiver performs the following tasks:

    • Retrieves the list of five language-specific Search Dropdown display group names from the feature properties
    • Identifies the default scope in the existing Search Dropdown display group
    • Creates any of the Search Dropdown display group that do not exist
    • Ensures that all compiled shared search scopes are added to each search dropdown display group

    The feature does NOT delete any of these display groups or their associated scopes upon deactivation, as it is not possible to determine which scopes were set manually. If additional language packs would be installed, we'd need to include the additional language-specific Search Dropdown display group names. Extending the feature to support additional languages simply requires adding a new property to the feature.xml <Properties/> element.

    The Code

    There are only two pieces to the solution - the feature manifest and the feature receiver. The example below just shows properties for two languages to keep things simple (and avoid problems handling unicode).

    Feature.xml:

    <?xml version="1.0" encoding="utf-8"?>

    <Feature
    Id="12345678-1234-1234-1234-1234567890AB"
    Title="ScopeDisplayGroups"
    Description="This feature is used to create the search scope display groups needed to enable enhanced search on multilanguage sites."
    Version="1.0.0.0"
    Scope="Site"
    Hidden="false"
    ReceiverAssembly="ScopeDisplayGroups, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1234123412341234"
    ReceiverClass="ScopeDisplayGroups.FeatureReceiver" xmlns="http://schemas.microsoft.com/sharepoint/">

    <Properties>
    <
    Property Key="en" Value="Search Dropdown"/>
    <
    Property Key="nl" Value="Vervolgkeuzelijst voor zoeken"/>
    </Properties>

    </Feature>

    Feature receiver:

    using System;

    using System.Web;

    using System.Web.UI;

    using System.Web.UI.WebControls;

    using System.Web.UI.WebControls.WebParts;

    using Microsoft.SharePoint;

    using Microsoft.SharePoint.WebControls;

    using Microsoft.SharePoint.WebPartPages;

    using Microsoft.SharePoint.Navigation;

    using Microsoft.SharePoint.Administration;

    using Microsoft.Office.Server.Search.Administration;

    namespace ScopeDisplayGroups {

    public class FeatureReceiver : SPFeatureReceiver {

    public override void FeatureActivated(SPFeatureReceiverProperties properties) {

     

    SearchContext searchContext;

    string currentGroupName = string.Empty;

    using (SPSite currentSite = (SPSite)properties.Feature.Parent)

    {

    //retrieve current scope display groups from this site

    searchContext = SearchContext.GetContext(currentSite);

    Scopes scopes = new Scopes(searchContext);

    ScopeDisplayGroupCollection scopeDisplayGroups = scopes.AllDisplayGroups;

    Uri siteUri = new Uri(currentSite.Url);

    ScopeDisplayGroup displayGroup = null;

    Scope defaultScope = null;

     

    //retrieve new scope display group labels from feature properties

    //first, iterate through the scope display group labels to find the one that exists

    //so we can identify the default scope

    foreach (SPFeatureProperty featureProperty in properties.Feature.Properties)

    {

    currentGroupName = featureProperty.Value;

    try

    {

    displayGroup = scopes.GetDisplayGroup(siteUri, currentGroupName);

    defaultScope = displayGroup.Default;

    }

    catch (InvalidCastException ex)

    {

    //do nothing if group is not found

    }

    }

    //retrieve new scope display group labels from feature properties

    foreach (SPFeatureProperty featureProperty in properties.Feature.Properties)

    {

    currentGroupName = featureProperty.Value;

    try

    {

    displayGroup = scopes.GetDisplayGroup(siteUri, currentGroupName);

    //if display group exists, add any missing compiled shared scopes

    //this is a "bonus feature", since API doesn't allow us to determine which scopes are OOTB

    //normally only All Sites and People are added automatically to the search dropdown feature

    foreach (Scope sharedScope in scopes.GetSharedScopes())

    {

    //only add shared scopes that are currently compiled AND are not part of display group

    if (!displayGroup.Contains(sharedScope) &&

    (sharedScope.CompilationState.Equals(ScopeCompilationState.Compiled)))

    displayGroup.Add(sharedScope);

    }

    displayGroup.Update();

    }

    catch (InvalidCastException ex)

    {

    //if display group does not exist, create the new scope display group

    ScopeDisplayGroup newDisplayGroup = scopeDisplayGroups.Create(currentGroupName, string.Empty, siteUri, true);

    foreach (Scope sharedScope in scopes.GetSharedScopes())

    {

    //only add shared scopes that are currently compiled

    if (sharedScope.CompilationState.Equals(ScopeCompilationState.Compiled))

    newDisplayGroup.Add(sharedScope);

    }

    //if one was found, set the default scope to the same value for all new display groups

    //AND move it to the first choice on the list

    if (defaultScope != null)

    {

    newDisplayGroup.Remove(defaultScope);

    newDisplayGroup.Insert(0, defaultScope);

    newDisplayGroup.Default = defaultScope;

    }

    newDisplayGroup.Update();

    }

    scopes.Update();

    }

    }

     

    }

    public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {

    /* no op */

    //do nothing - removing search scope display groups could be destructive

    }

    public override void FeatureInstalled(SPFeatureReceiverProperties properties) {

    /* no op */

    }

    public override void FeatureUninstalling(SPFeatureReceiverProperties properties) {

    /* no op */

    }

    }

    }

  • Addressing Vanity URLs in a Content Migration

    It's been a blistering few months prepping for MCM, going through the MCM program, and getting caught up on the massive backlog of work and life that occurs when you tell coworkers, friends, and family that you'll be dropping off the face of the earth for three weeks. :)  Now that I finally have a breather to start assimilating some of the novel and interesting lessons learned on my recent projects, here goes item #1...

    The Problem

    We've been migrating a large corporate intranet from Lotus Notes to MOSS. As you may have surmised from my previous post about maintaining link integrity, one of the many problems we needed to address was updating existing Lotus Notes URLs in the content to their new MOSS-based URLs.

    At a high level, the migration tools executed a two-stage process - convert content from Notes to SharePoint, then update any Notes links in the content to reference their new SharePoint links. However, there was one class of heavily utilized links that weren't in the content, and therefore couldn't be updated by any tool - these were hundreds of "vanity URLs" that were referenced in thousands of users' bookmarks and in unknown numbers of non-Notes systems and files. For example, these were URLs like http://vacation (redirecting to http:/{notes server address for the vacation policy page in an HR intranet site}) or http://someeventname  (redirecting to the announcement page for some big annual company event).

    Before the migration, this is the way these vanity URLs worked:

    • DNS mapped the vanity URL to the VIP for the Notes servers
    • (black box, but you get the idea) The Notes server contained a mapping of a particular host header to a deep URL.

    Since these aliases weren't content, there was no way to convert them. Also, since there were hundreds of vanity URLs, we needed a way to implement the redirects in such a way that the migration tool would be able to update the redirect links in an automated fashion. As a secondary goal, many of these vanity URLs had been around for ages and were due for a cleanup, so a convenient means of tracking their usage would aid in retiring the unused ones, which incurred additional configuration overhead.

    The Solution

    We already had a tool for updating Notes links in SharePoint content to the new format/location, so the simplest solution was to transform the redirects into SharePoint content. Here are the basic requirements and approaches we took to address them:

    • Requirement: Map host headers to SharePoint content with a minimum of overhead
      • Approach: Use host header site collections as the basis for this solution. One host header site collection per vanity URL.
    • Requirement: Minimize configuration effort for creating the content
      • Approach: Script the creation of the host header site collections via STSADM.
    • Requirement: Implement a standard way of configuring redirects 
      • Approach: Implement a standard "redirect control" on the host header site collection home pages that used configurable settings to execute a redirect to an arbitrary URL.
    • Requirement: Minimize configuration effort for implementing the redirects
      • Approach: Enable script-based configuration of the redirect settings via STSADM.
    • Requirement: Minimize configuration effort for permissioning the redirect sites for all intranet users (anonymous and authenticated)
      • Approach: Enable script-based configuration of anonymous authentication settings via STSADM.
    • Requirement: Minimize the payload of redirect pages
      • Approach: Apply a master page to the host header site collections that renders an absolute minimum of content.
    • Requirement: Enable vanity URL owners to track their usage
      • Approach: Having one host header site collection per vanity URL provides standard SharePoint usage tracking info that is available to each site owner.
    • Requirement: Keep it simple
      • Approach: Create a new "redirect site" site definition that is based largely on the Blank Site site definition.

    So to summarize, we would create a new site definition based on the Blank Site site definition with a lean, mean master page, and a built-in redirect control on the default page. We'd create these sites as host header site collections via STSADM and extend STSADM to enable us to script their creation and the configuration of the redirect links. There's a little more to this on the infrastructure side (DNS and IIS configuration are the most notable), but to keep the focus on the SharePoint part of the solution, the key thing to note is that once the new SharePoint sites were in production, the key cutover task was to change the DNS records for the alias sites from the Notes VIP to the SharePoint farm VIP.

    The Design

    There are two sets of components to the design - the UI components and the management features.

    UI Components

    The Redirect Site solution consists of a site definition, a feature, and a web control.

    The site definition is a copy of the Blank Site site definition, with the following change:

    ·         Default.aspx

    o   The SimpleRedirect.ascx control was registered with the page.

    o   A reference to the SimpleRedirect control was added to the PlaceHolderAdditionalPageHead placeholder.

    o   The master page was changed to empty.master.

    ·         ONET.xml

    o   Elements that are not relevant to the Blank Site site definition (i.e. Team Site and Document Workspace-only elements) were removed

    o   A <Feature/> element for the Redirect Site Master Page Feature was added.

    The Redirect Site Master Page Feature installs the empty.master master page in the master page library for the Redirect Site. All placeholders on the master page are set with Visible=”false”.

    The SimpleRedirect.ascx web control renders a simple META REFRESH tag (http://msdn.microsoft.com/en-us/library/bb159711.aspx ), using the following property bag values:

    ·         REDIRECT_URL – The URL to be loaded.

    ·         REDIRECT_TIME – The delay in seconds to be applied before loading the REDIRECT_URL.

    When users navigate to the base URL for a redirect site, the default page loads, using the very streamlined empty.master page, and rendering the SimpleRedirect web control, causing them to be redirected to the REDIRECT_URL within REDIRECT_TIME seconds.

    Both the site definition and the feature are hidden, as they are never intended for use by end users.

    Management Components

    The management functionality required two new commands - setbagproperty and setanonymousaccess - and a third was added for convenience (getbagproperty).

    The setbagproperty command has the following syntax:

    stsadm -o setbagproperty -url <url> -propertykey <keyname> -propertyvalue <value>

    Where the placeholders above have the following usage:

    ·         url – the URL to the SPWeb to be updated

    ·         keyname – the property key to be created (if it does not exist) or updated (if It does exist)

    ·         value – the value to be set for the specified property key

    Example – setting the value of the “test” property to “thisvalue” for the web http://intranet.example.com/sites/testsite :

    stsadm -o setbagproperty -url http://intranet.example.com/sites/testsite -propertykey test -propertyvalue thisvalue

    The getbagproperty command has the following syntax:

    stsadm -o setbagproperty -url <url> -propertykey <keyname>

    Where the placeholders above have the following usage:

    ·         url – the URL to the SPWeb to be updated

    ·         keyname – the property key whose value is to be retrieved

    Example – retrieving the value of the “test” property for the web http://intranet.example.com/sites/testsite :

    stsadm -o setbagproperty -url http://intranet.example.com/sites/testsite -propertykey test

    The setanonymousaccess command has the following syntax:

    stsadm -o setanonymousaccess -url <url> -state <state>

    Where the placeholders above have the following usage:

    ·         url – the URL to the SPWeb whose anonymous access settings are to be configured

    ·         state – anonymous access state, which should be one of the following:

    o   0 – Disabled: Specifies that anonymous users have no access to a Web site.  

    o   1 – Enabled: Specifies that anonymous users can access lists and libraries if the lists and libraries allow anonymous access.

    o   2 – On: Specifies that anonymous users can access the entire Web site. This setting means that the value of the AnonymousPermMask64 property depends on the permission mask for the Limited Access role.

    Example – enabling anonymous users to access lists and libraries if the lists and libraries allow anonymous access for the web http://intranet.example.com/sites/testsite :

    stsadm -o setanonymousaccess -url http://intranet.example.com/sites/testsite -state 1

    The Code

    Here are a few of the novel components of the solution in case you find them useful - STSADM extensions and the redirect control.

    SimpleRedirect.ascx

    <%@ Control Language="C#" compilationMode="Always" %>

    <%@ Import Namespace="Microsoft.SharePoint" %>

    <%@ Import Namespace="Microsoft.SharePoint.WebControls" %>

    <%@ Import Namespace="Microsoft.SharePoint.Utilities" %>

    <%

     

    SPWeb web = SPControl.GetContextWeb(Context);

    string strRedirectMetaTag = string.Empty;

     

    if ((web.Properties.ContainsKey("REDIRECT_URL")) && (web.Properties.ContainsKey("REDIRECT_TIME")) )

    {

    strRedirectMetaTag = "<META http-equiv=\"REFRESH\" content=\"" + web.Properties["REDIRECT_TIME"] + ";url=" + SPHttpUtility.HtmlEncode(web.Properties["REDIRECT_URL"]) + "\">";

    }

     

    %>

    <%=strRedirectMetaTag%>

    GetBagProperty.cs:

    using System;

    using System.Collections.Generic;

    using System.Text;

    using Microsoft.SharePoint.StsAdmin;

    using Microsoft.SharePoint;

    namespace SharePoint.StsAdmin

    {

    public class GetBagProperty : ISPStsadmCommand

    {

    #region ISPStsadmCommand Members

    const string USAGE_SYNTAX = "stsadm -o getbagproperty -url <url> -propertykey <keyname>";

    public string GetHelpMessage(string command)

    {

    return USAGE_SYNTAX;

    }

    public int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)

    {

    output = string.Empty;

    string strWebUrl = string.Empty;

    string strPropKey = string.Empty;

    //read in the arguments

    if (keyValues.ContainsKey("url"))

    {

    strWebUrl = keyValues["url"];

    }

    if (keyValues.ContainsKey("propertykey"))

    {

    strPropKey = keyValues["propertykey"];

    }

    //confirm that all required arguments are present

    if ((strPropKey.Length > 0) && (strWebUrl.Length > 0))

    {

    try

    {

    using (SPSite siteCol = new SPSite(strWebUrl))

    {

    //use the utility to obtain the actual web's url

    string webUrl = Utility.GetWebUrl(strWebUrl, siteCol.ServerRelativeUrl);

    using (SPWeb web = siteCol.OpenWeb(webUrl))

    {

    if (web.Properties.ContainsKey(strPropKey))

    output += string.Format("Property found. Value: {0}", web.Properties[strPropKey]);

    else

    output += "Property does not exist.";

    }

    }

    }

    catch (Exception ex)

    {

    output += ex.Message;

    }

    }

    else

    {

    output += "Missing argument.\r\n" + USAGE_SYNTAX;

    }

    return 0;

    }

    #endregion

    }

    }

    SetBagProperty.cs:

    using System;

    using System.Collections.Generic;

    using System.Text;

    using Microsoft.SharePoint.StsAdmin;

    using Microsoft.SharePoint;

    namespace SharePoint.StsAdmin

    {

    public class SetBagProperty : ISPStsadmCommand

    {

    #region ISPStsadmCommand Members

    const string USAGE_SYNTAX = "stsadm -o setbagproperty -url <url> -propertykey <keyname> -propertyvalue <value>";

    public string GetHelpMessage(string command)

    {

    return USAGE_SYNTAX;

    }

    public int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)

    {

    output = string.Empty;

    string strWebUrl = string.Empty;

    string strPropKey = string.Empty;

    string strPropValue = string.Empty;

    //read in the arguments

    if (keyValues.ContainsKey("url"))

    {

    strWebUrl = keyValues["url"];

    }

    if (keyValues.ContainsKey("propertykey"))

    {

    strPropKey = keyValues["propertykey"];

    }

    if (keyValues.ContainsKey("propertyvalue"))

    {

    strPropValue = keyValues["propertyvalue"];

    }

    //confirm that all required arguments are present

    if ((strPropValue.Length > 0) && (strPropKey.Length > 0) && (strWebUrl.Length > 0))

    {

    try

    {

    using (SPSite siteCol = new SPSite(strWebUrl))

    {

    //use the utility to obtain the actual web's url

    string webUrl = Utility.GetWebUrl(strWebUrl, siteCol.ServerRelativeUrl);

    using (SPWeb web = siteCol.OpenWeb(webUrl))

    {

    if (web.Properties.ContainsKey(strPropKey))

    {

    output += "Key exists - updating.\r\n";

    web.Properties[strPropKey] = strPropValue;

    }

    else

    {

    output += "Key does not exist - adding.\r\n";

    web.Properties.Add(strPropKey, strPropValue);

    }

    web.Properties.Update();

    }

    }

    output += "Property set.";

    }

    catch (Exception ex)

    {

    output += ex.Message;

    }

    }

    else

    {

    output += "Missing argument.\r\n" + USAGE_SYNTAX;

    }

    return 0;

    }

    #endregion

    }

    }

    SetAnonymousAccess.cs:

    using System;

    using System.Collections.Generic;

    using System.Text;

    using Microsoft.SharePoint.StsAdmin;

    using Microsoft.SharePoint;

    namespace SharePoint.StsAdmin

    {

    public class SetAnonymousAccess : ISPStsadmCommand

    {

    #region ISPStsadmCommand Members

    const string USAGE_SYNTAX = "stsadm -o setanonymousaccess -url <url> -state <state>"

    + "\r\n\r\n\tState Values:" + "\r\n\t0: Disabled" + "\r\n\t1: Enabled" + "\r\n\t2: On";

    public string GetHelpMessage(string command)

    {

    return USAGE_SYNTAX;

    }

    public int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)

    {

    output = string.Empty;

    string strWebUrl = string.Empty;

    string strState = string.Empty;

    string strNewState = string.Empty;

    int state = -1;

    //read in the arguments

    if (keyValues.ContainsKey("url"))

    {

    strWebUrl = keyValues["url"];

    }

    if (keyValues.ContainsKey("state"))

    {

    strState = keyValues["state"];

    try

    {

    state = Convert.ToInt32(strState);

    }

    catch (FormatException fex)

    {

    output += "Invalid argument for state parameter.";

    return 0;

    }

    catch (OverflowException oex)

    {

    output += "Invalid argument for state parameter.";

    return 0;

    }

    }

    //confirm that all required arguments are present and valid

    if ((state > -1) && (state < 3) && (strWebUrl.Length > 0))

    {

    try

    {

    using (SPSite siteCol = new SPSite(strWebUrl))

    {

    //use the utility to obtain the actual web's url

    string webUrl = Utility.GetWebUrl(strWebUrl, siteCol.ServerRelativeUrl);

    using (SPWeb web = siteCol.OpenWeb(webUrl))

    {

    if (state == 0)

    web.AnonymousState = SPWeb.WebAnonymousState.Disabled;

    else if (state == 1)

    web.AnonymousState = SPWeb.WebAnonymousState.Enabled;

    else if (state == 2)

    web.AnonymousState = SPWeb.WebAnonymousState.On;

    strNewState = web.AnonymousState.ToString();

    }

    }

    output += "Anonymous access state set to " + strNewState;

    }

    catch (Exception ex)

    {

    output += ex.Message;

    }

    }

    else

    {

    output += "Missing or invalid argument.\r\n" + USAGE_SYNTAX;

    }

    return 0;

    }

    #endregion

    }

    }

  • Preventing and Managing Broken Links in SharePoint

    Link Integrity Management: Processes, Tactics, and Solutions

    One lengthy and complex topic I recently discussed with a customer is link integrity management – how to keep links from breaking when you move and rename things. Turns out there are several dimensions to this problem and a wide array of tactics for addressing the many scenarios that can lead to broken links. I did a bit of research around the web and found quite a few processes, tactics, and a couple of technical solutions to mitigate these issues. We’re using a combination of all of them for this intranet publishing & collaboration deployment, so I thought I’d share the generic recommendations we came up with. Here goes…

    The solutions for addressing link integrity issues typically include one or more of the following types of components:

    ·         Training to prevent and reduce the occurrence of link integrity issues

    ·         Proactive business processes to prevent link integrity issues

    ·         Reactive business processes to correct link integrity issues

    ·         Tools to identify link integrity issues reactively or proactively

    ·         Tools to avoid or mitigate the impact of link integrity issues

    Evaluating Effectiveness

    When you’re comparing these options, you need to examine two aspects of their effectiveness: link sources and link destinations.

    Link sources are the container for a link, and I decomposed these into the following four types:

    ·         Intra-Site Collection

    ·         Intra-SharePoint

    ·         External Web Sites

    ·         Bookmarks/Files

    The link source matters because certain solutions rely on detecting a broken link and fixing it at the source – i.e. changing http://old to http://new – and certain solutions can only affect specific sources.

    Link destinations are the endpoint of the link – I decomposed these into the following eight groups of SharePoint objects:

    ·         Site Collections

    ·         Sites

    ·         Libraries & Lists

    ·         Folders

    ·         Content Pages (AKA Publishing Pages)

    ·         Web Part & Basic Pages

    ·         Files

    ·         Items

    As with link sources, certain solutions can only affect specific endpoints, as they rely on applying some type of redirection to reroute traffic from http://old to http://new .

    Of course, effectiveness is the “benefits” side of the equation, so you also need to factor in the “costs” side as well, which range from free to hugely expensive (which isn’t always obvious). Now onto the solutions and my thoughts on each…

    Site Design Practices

    Site design practices should be part of standard SharePoint training for content authors and site owners. Renaming and moving content can often be avoided by using standard SharePoint navigation controls and features for organizing and viewing content. When moving and renaming content is required, a number of built-in tools for updating links, implementing redirects and analyzing the impact of these changes can mitigate the scope of link integrity issues.

    Example practices for avoiding/mitigating link integrity issues include:

    ·         Avoid renaming documents & files. Use the Title field – don’t rename the file.

    ·         Avoid restructuring libraries & lists

    o   Avoid folders or keep folder structures SIMPLE so that they are less likely to change

    o   Use views and metadata to organize contents instead

    o   Don’t use document libraries simply to categorize documents – different libraries should be created to support different metadata, permissions, policies, workflows etc.

    ·         Always implement a Redirect Page when moving Content Pages. The redirect page is a standard page layout that content authors can use to quickly implement a redirect.

    ·         Always implement a Content Editor Web Part-based redirect when moving Web Part Pages or Basic Pages. Savvy users can insert an HTML snippet in Content Editor Web Parts to implement a redirect to another page.

    ·         Always have a communications plan for your site – notify users well in advance of any major restructuring

    ·         Use the site usage reports PROACTIVELY to notify users who may be impacted by site restructuring

    ·         Review Redirect Page traffic in usage reports to determine if they are still needed

    ·         Review 404 reports to identify users and sites with broken links. This requires a 404 reporting solution as described later.

    ·         Review 404 reports to identify needed redirect pages. This requires a 404 reporting solution as described later.

    Solution Scope

    Site Design Practices mitigate broken link issues across all link sources: intra site collection, intra-SharePoint, other web sites, and bookmarks/files. They don’t eliminate issues because they are largely dependent on people and processes, but the adherance to those processes is what counts.

    Site Design Practices are effective for all major link destinations: Site Collections, Sites, Libraries & Lists, Folders, Content Pages, Web Part & Basic Pages, Files, and Items

    Analysis

    Good site design practices reduce the occurrence of link integrity issues on all scopes and components. Good site management practices – in the form of communication plans and usage report analysis – reduce the impact of all link integrity issues whenever they occur.  These best practices have negligible additional cost in relation to generally recommended training deliveries for site owners and content authors.

    For more information

    http://technet.microsoft.com/en-us/library/cc261852.aspx

    404 Reporting and Fixing – Broken Link Crawler

    A broken link crawler is a tool that crawls one or more web sites, checks for 404 errors on all accessible pages in the site, and produces a report of the source and destination URLs for those 404 errors. They also typically produce a list of all links crawled and have mechanisms for seeking particular URL patterns.

    Popular broken link crawler tools include:

    ·         Xenu Link Sleuth

    ·         LinkScan

    Some of these tools can be used proactively to crawl content for a base URL in preparation for a move. These reports enable the content owners with links to content that is going to be moved/renamed to update their links in advance or otherwise time the update of their links to coincide with the move of the external site.

    All of these tools can be used reactively to identify broken links in these sites. These reports enable the content owners with broken links to identify content in need of updating.

    Solution Scope

    Broken link crawlers can eliminate broken link issues across three link sources: intra site collection, intra-SharePoint, and KNOWN web sites. They are ineffective for dealing with links in user bookmarks/files, and they may encounter problems parsing some file formats and working within some security frameworks.

    Broken link crawlers are effective for all major link destinations: Site Collections, Sites, Libraries & Lists, Folders, Content Pages, Web Part & Basic Pages, Files, and Items

    Analysis

    Proactive use of these broken link crawlers is appropriate in advance of a major site collection or site restructuring, or major structural change to a very large document library. Site owners must be trained in advance to plan to request these reports. These reports are computationally expensive to produce since they add traffic to all systems examined by the tool, so they are not for casual use.

    Reactive use of broken link crawlers should be conducted on a regular basis – i.e. weekly, monthly, quarterly reports that are submitted to all site owners of the source content site. This empowers site owners to better manage their site content.

    Broken link crawlers cannot affect change for unreported sites and systems, nor can they scan user bookmarks – they can only produce reports for web sites that are accessible to the crawler.

    Some of these tools are free, but the resources required to use them and integrate them into administration processes are not. Custom solutions are typically required to direct these reports to the appropriate site owners. They offer the most benefits for large migrations or restructuring efforts for sites that primarily render HTML content.

    404 Reporting and Fixing – 404 Report Analysis

    “404 reports” can be obtained directly from the IIS logs of MOSS servers. These logs show what broken links are receiving traffic and also importantly, who is attempting to access these URLs. Reports created from this data can easily identify the most frequently accessed 404 pages.

    Solution Scope

    404 reporting & fixing can mitigate broken link issues across all link sources: intra site collection, intra-SharePoint, web sites, and user bookmarks/files.

    404 reporting & fixing is effective for all major link destinations: Site Collections, Sites, Libraries & Lists, Folders, Content Pages, Web Part & Basic Pages, Files, and Items

    Analysis

    Using 404 reports to address broken links is purely reactive measure, but it works for all logical architecture components. For existing sites, site owners can review the 404 reports and contact the user(s) who hit the broken links or, in the case of pages, implement a redirect. For sites that have moved, farm administrators can stake the same actions, since there would be no clear owner to a site that no longer exists at a given URL.

    404 reports are not readily available to site owners, and are not easily compiled by farm administrators (especially in environments with more than one web front end server). Delivering 404 reports to both sets of users would require a custom solution which could also leverage a tool like LogParser or WebTrends to compile the reports.

    For more information

    http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/IIS/ab7e4070-e185-4110-b2b1-1bcac4b168e0.mspx?mfr=true

    http://www.microsoft.com/downloads/details.aspx?FamilyID=890cd06b-abf8-4c25-91b2-f8d975cf8c07&displaylang=en

    “Smart” 404 Page

    The standard 404 page simply presents a “404-page not found” message to the user. A “smart” 404 page would improve the user experience by providing richer features for finding the content the user is seeking, although not address the root cause of the missing object.

    Solution Scope

    A Smart 404 Page can mitigate broken link issues across all link sources: intra site collection, intra-SharePoint, web sites, and user bookmarks/files.

    A Smart 404 Page is effective for all major link destinations except Site Collections: Sites, Libraries & Lists, Folders, Content Pages, Web Part & Basic Pages, Files, and Items. Attempts to access a moved/missing site collection root will result in an ordinary 404 page.

    Analysis

    A smart 404 page would, at a minimum, include SharePoint search results based on the terms in the URL that produced the 404 error. It could also include basic information, such as whether the site, list, or other container object currently exists or has been moved; or contact information for an existing site. Further, a smart 404 page could be used to trigger events for reactive measures, like notifying site owners or farm administrators.

    A smart 404 page is a relatively simple customization to implement, as it could be based on an existing Codeplex project (http://www.codeplex.com/sharepointsmart404 ). Each component added to the Smart 404 page beyond the search results may require additional work.

    For more information

    http://www.codeplex.com/sharepointsmart404

    http://www.sharepointbrainfreeze.com/archive/2008/09/29/creating-a-enhanced-moss-404-feature-part-1.aspx

    http://blogs.msdn.com/jingmeili/archive/2007/04/08/how-to-create-your-own-custom-404-error-page-and-handle-redirect-in-sharepoint-2007-moss.aspx

    Content Page Redirects

    The Redirect Page is a standard page layout included in the MOSS Publishing Features that content authors can use to quickly implement a redirect. Authors use it to capture the new URL for a content page, whereupon the redirect mechanism on the page causes the user’s request to be redirected to the new URL if they hit the Redirect Page.

    Though they can be implemented manually, one customization that could simplify the task of moving a single content page would be offering a new custom action that collapses the tasks of moving a content page and constructing a Redirect Page into a single step.

    Solution Scope

    Content Page Redirects can eliminate broken link issues across all link sources: intra site collection, intra-SharePoint, web sites, and user bookmarks/files.

    Content Page Redirects are only effective for Content Page objects.

    Analysis

    Content Page Redirects can always be created manually. A “move and create redirect page” custom action would be an alternative available to content authors in addition to the standard tools they could use for moving content pages in SharePoint, such as Explorer View and SharePoint Designer. This action would provide link integrity across any scope for any content page moved in this manner.

    Generation of redirect pages for content page moves should NOT be automated, as that could generate massive numbers of redirect pages, thereby slowing down requests, increasing storage requirements, and greatly increasing content complexity. Nearly all redirects are intended to be used for a limited period of time until communications have caught up with restructuring actions, so automated generation should be optional, not automatic.

    Content authors on publishing sites must be trained in the use of Redirect Pages. Developing a custom action to simplify the process of cross site-collection moves of content pages would require a relatively simple customization – adding a custom Page Editing Menu item and a custom action associated with it.

    For more information

    http://office.microsoft.com/en-us/sharepointserver/HA101575641033.aspx

    http://www.toddklindt.com/blog/Lists/Posts/Post.aspx?List=56f96349%2D3bb6%2D4087%2D94f4%2D7f95ff4ca81f&ID=48

    Web Part Page Redirects

    Web part pages and basic pages don’t have an equivalent to the Redirect page layout, but users can insert an HTML snippet in Content Editor Web Parts on web part pages to implement a redirect to another page. Content authors of content pages that contain web part zones can use the same approach for implementing a redirect.

    Implementing this type of redirect requires some HTML knowledge, though a relatively simple custom “Redirect” web part could simplify this task and make it readily available for all users.

    Solution Scope

    Web Part Page Redirects can eliminate broken link issues across all link sources: intra site collection, intra-SharePoint, web sites, and user bookmarks/files.

    Web Part Page redirects are only effective for Content Pages, Web Part Pages, and Basic Pages.

    Analysis

    These types of redirects work for all scopes for all content pages, web part pages, and basic pages to which they are applied. A custom redirect web part would include, at a minimum, a redirect message, a redirect URL, and a time period for pausing before the redirect action occurs. These parameters would be simple configuration settings for the web part.

    Implementing a custom web part with these specifications would be a simple customization that would be considerably more cost-effective than supporting training issues relating to authoring HTML redirects or correcting issues with HTML redirects.

    For more information

    http://www.toddklindt.com/blog/Lists/Posts/Post.aspx?List=56f96349%2D3bb6%2D4087%2D94f4%2D7f95ff4ca81f&ID=48

    Site Collection/Site Redirects – IIS-based

    An IIS-based redirect is an IIS virtual directory that uses a pattern match of arbitrary complexity to redirect URLs matching that pattern to other URL– for example, redirecting all traffic to http://sharepoint.ing.com/sites/oldsite to http://sharepoint.ing.com/sites/newsite for all URLs in oldsite.

    Solution Scope

    IIS-based redirects initially eliminate broken link issues across all link sources: intra site collection, intra-SharePoint, web sites, and user bookmarks/files, but their quality may degrade over time.

    IIS-based redirects areeffective for all major link destinations within their associated container (a site collection or site): Site Collections, Sites, Libraries & Lists, Folders, Content Pages, Web Part & Basic Pages, Files, and Items.

    Analysis

    IIS-based redirects work for all scopes and object types for the base URL used in the redirect, whether that is a site collection, site, or lower logical architecture component. This is an incomplete solution - if any of the contents in the redirect destination are moved or been renamed after the redirect is imposed, those contents may result in broken links.

    Since IIS-based redirects require creating a new virtual directories on each MOSS server to implement the redirect, this approach does not scale to large numbers of redirects. It should only be implemented for critical sites or for very limited periods of time for other sites, and should only be used at the site or site collection level.

    This approach requires no customizations, but does require training and operations documentation for server administrators.

    For more information

    http://www.toddklindt.com/blog/Lists/Posts/Post.aspx?ID=48

    http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/IIS/41c238b2-1188-488f-bf2d-464383b1bb08.mspx?mfr=true

    Hope these help… happy content authoring!

  • Managing Web.Config Customizations

    I'm back after a long hiatus - international relocations tend to consume a lot of your free time - so it's time to start dusting off some of the many learnings I've had over the past few months...

    One important topic I've encountered with my current and previous customers is how to manage web.config updates when deploying customizations. If you've been playing around with the Solutions framework, you're probably familiar with the ease of adding SafeControls entries to your web.config via your solution definition file, but what about all those other configuration settings that inevitably come up with SharePoint customizations?

    In my research, I found three tactics for implementing generic web.config customizations programmatically:

    1. Install customizations in the \12\CONFIG directory as specified in the WSS SDK: http://msdn.microsoft.com/en-us/library/ms439965.aspx 
    2. Use the SPWebConfigModification class
    3. Manipulate the web.config file directly as an XML file (yuck!)

    Based on the respective capabilities and level of effort associated with each tactic, I assembled the following matrix of tactics to customization scenarios - hope this helps!

     

    Customization Scope

    Method

    Example

    All zones for all Web Applications (Content web apps and Central Administration)

    Create a solution with a webconfig.customizationname.xml file in the \12\CONFIG directory. See http://msdn.microsoft.com/en-us/library/ms439965.aspx for details.

     

    Deploy this solution using an ApplicationServer deploymentservertype.

     

    Execute the stsadm –o copyappbincontent command on all web application hosts (and the Central Admin host, if applicable) to apply retroactively to all existing content web application zones.

     

    Notes:

    ·         Do not execute the copyappbincontent command multiple times on the same server, as this can create duplicate entries of the customizations.

    ·         You do not need to execute the copyappbincontent command to propagate the customization new zones/web apps.

     

    A generic SPSite or SPWeb-scoped feature that is activated via a FeatureSiteTemplateAssociation.

    All zones for all Web Applications (Content web apps only)

    Create a solution with a webconfig.customizationname.xml file in the \12\CONFIG directory. See http://msdn.microsoft.com/en-us/library/ms439965.aspx for details.

     

    Deploy this solution using a WebFrontEnd deploymentservertype.

     

    Execute the stsadm –o copyappbincontent command on all web application hosts to apply retroactively to all existing content web application zones.

     

    Notes:

    ·         Do not execute the copyappbincontent command multiple times on the same server, as this can create duplicate entries of the customizations.

    ·         You do not need to execute the copyappbincontent command to propagate the customization new zones/web apps.

     

    A generic SPSite or SPWeb-scoped feature that is activated manually by a site owner/administrator.

    All zones for selected Content Web Application(s)

    Create a solution with a feature based on the SharePointDebugger feature. See http://blog.tedpattison.net/default.aspx for a detailed overview and http://tedpattison.net/downloads.aspx to download.

    ·         FeatureReceiver.cs - Update the modification.Owner property as appropriate

    ·         FeatureReceiver.cs - Update the Entries property to capture all required modifications. Be sure to use the correct SPWebConfigModificationType for each entry.

    ·         Feature.xml –update the ID, Title, Description, ImageUrl, ReceiverAssembly, and ReceiverClass as appropriate

     

    Deploy this solution using a WebFrontEnd deploymentservertype.

     

    Activate this feature on the appropriate web application(s) via the stsadm –o activatefeature command.

     

    An SPSite or SPWeb-scoped feature for personal sites (you did remember to put these on a separate web application, right?) that is activated manually by a personal site owner.

    Selected zone(s) for selected Web Application(s)

    A manual update is the best practice unless automation is absolutely required.

     

    It is possible to update the web.config for a specific zone on all servers through code, but this essentially requires direct manipulation of the web.config files as XML documents.

     

    Configuration settings for forms authentication providers.

     

  • New Advanced SharePoint Webcasts...

    Three of the SharePoint Academy and BPIO University courses I taught earlier this year are now available in webcast format. Many thanks to the sponsors on our product team, the Microsoft Information Worker community (global and local), content contributors, and MS Studios for making these possible. Now you can see what I look like when I'm reading from a teleprompter. :)

    These webcasts contain a mix of standard material from the respective courses, as well as a number of my own contributions. Many of my contributions focus on material from areas of focus in my blog - customization, master pages, features, solutions, etc. - so thanks to all of you who've helped me enhance the content by sharing feedback, questions, and corrections to the original blogs.

    I'm sure they'll end up on a landing page somewhere soon, but here are the public links:

    Microsoft SharePoint Technologies Solution Architecture

    http://www.microsoft.com/winme/0805/32847/mod4/index.html

     

    Customizing and Extending Microsoft SharePoint Products and Technologies

    http://www.microsoft.com/winme/0805/32847/mod9/index.html

     

    Configuring the Business Data Catalog

    http://www.microsoft.com/winme/0805/32847/mod10/index.html

    Enjoy!

  • Branding Design using Solutions

    In a previous post, I extolled the virtues of deploying our branding code in a solution. While I still make that recommendation without any reservations, it's important to note that you may need TWO solutions to deploy your branding customizations.

    History 

    First, let's do a quick review of some of the generic components I used in branding our MOSS environment (using the HTTP module approach for LAYOUTS customizations):

    • Customizations to the GAC 
      • Feature Receiver DLL for the branding features
      • Some custom web part assemblies
      • Redirect Module DLL
    • Customizations to the "12" directory:
      • Custom application.master (applied via a redirect)
      • Custom simple.master (applied via a redirect)
      • Custom images
      • Custom Theme
      • A branding feature for Meeting Workspaces
      • A branding feature for all the other site templates
      • Branding feature stapler 
    • Customizations to web.config:
      • SafeControls entries for the custom web parts
      • Redirect module configuration info

    Everything except the redirect module configuration info (which we configured manually) is easily accomodated by the Solution framework, so that's exactly what I put in the original SINGLE branding solution file. So deploy all that stuff to your Web Front End servers, and you're good to go, right? Not entirely...

    The Problem...

    Two odd symptoms occurred in our regression testing. When we created sites/site collections from self-service site creation, STSADM on the WFE servers, or from the Create Page in a site, branding was applied. This is good. :) However, when we created site collections via STSADM from an app server or from Central Administration > Application Management > Create Site Collection, the branding was missing. This is bad. :)

    Why? The Central Admin server - and all non-WFE servers - were missing the branding code. The feature stapler and everything referenced by it - the branding features, the feature receiver (since the features referenced it), and the custom theme (since the feature receiver attempted to set a new site's theme to the custom theme) - need to be on these servers in order for the site creation to complete with all branding applied.

    The Solution...

    In case you didn't know already (I didn't, which was the cause of this issue), there are two types of Solution deployments - Web Front End (which only pushes the changes to servers hosting the web application role) and Application Server (which pushes changes to ALL SharePoint servers in the farm). I don't remember the complete solution manifest spec, but one other restriction that's relevant to this scenario is that only the Web Front End deployment type supports creation of SafeControl entries.

    Since I had a set of code that needed to be on ALL the servers as well as a couple of SafeControl entries, the simplest thing to do was to divide the customizations into two separate solutions - "core" branding and "web server" branding. The core solution holds all the components that are required on all servers, while the web server contains only the components that are needed to render the branded sites (like images, web parts, and LAYOUTS customizations).

    Here's the breakdown:

    "Core" Branding Solution

  • Customizations to the GAC
    • Feature Receiver DLL for the branding features
    • Redirect Module DLL
  • Customizations to the "12" directory:
    • Custom Theme
    • A branding feature for Meeting Workspaces
    • A branding feature for all the other site templates
    • Branding feature stapler 

    "Web Server" Branding Solution

  • Customizations to the GAC 
    • Some custom web part assemblies
  • Customizations to the "12" directory:
    • Custom application.master (applied via a redirect)
    • Custom simple.master (applied via a redirect)
    • Custom images
  • Customizations to web.config:
    • SafeControls entries for the custom web parts

    For ease of maintenance, I still used WSPBuilder for creating the manifests and .WSP files - this just required a minor tweak to the build script, which now looks something like this:

    REM build core server customizations
    wspbuilder.exe -solutionid 12345678-1234-1234-1234-1234567890ab -deploymentservertype ApplicationServer -wspname core-branding.wsp -excludefiles "a_list_of_all_the_wfe_files.txt" 

    REM build web server customizations
    wspbuilder.exe -solutionid 12345678-1234-1234-1234-1234567890ac -deploymentservertype WebFrontEnd -wspname webserver-branding.wsp -excludefiles "a_list_of_all_the_core_files.txt"

    Enjoy...

  • Adding An "All Authenticated Users" Feature to Forms-Based Authentication

    Preamble... 

    One of the frequenly cited shortcomings of Forms-Based Authentication when compared to Windows Integrated Authentication is its lack of built-in support for an "All Authenticated Users" group similar to NT AUTHORITY\Authenticated Users.

    This type of role comes in handy in a number of situations, most notably for provisioning public (but not anonymous) sites and public SSP permissions like "Create Personal Site". The most common workaround I've encountered has been a custom solution that ties in with user provisioning - create a role in the directory service, add all existing users to it, add any new users to it, and remove any users from it as they're deleted. For very large-scale directories, it's not practical to store many thousands of users in a single group, so the actual implementation might be one involving nested groups or a denormalized structure where group memberships are enumerated in the user object, but the same idea applies.

    Problem...

    My present customer's extranet MOSS implementation uses a very large LDAP directory with hundreds of thousands of potential users. To deal with their problems of scale, they long ago adopted the "list groups in the user object" approach. This made us unable to use the out-of-the-box MOSS Role Manager, which expects to find users enumerated in group objects, rather than groups enumerated in user objects. This led to a need for a custom LDAP Role Manager.

    Like most MOSS implementations, they have a need for an "all authenticated users" role, but modifying user provisioning processes in their LDAP (of which there are many, but that's another story) wasn't a realistic option in terms of cost or schedule. Fortunately, the custom Role Manager opened the door to an elegant solution to the "All Authenticated Users" requirement.

    Solution...

    Once you've written your own role manager, it's pretty straightforward to embed your own "All Authenticated Users" feature into the component. I found it easiest to make this a configurable group name, but you could just as easily hard-code the value. Here's the walkthrough:

    1. Create a private class variable (_LDAPAuthUsersGroup in the example below) for storing the group name.

    2. Populate the group name variable in the Initialize method via a configuration setting.

    3. In the GetRolesForUser method, append the "All Auth Users" group name to the roles list every time, thereby making every user a member of the group.

    4. In the RoleExists method, always return true for the "All Auth Users" group name, thereby confirming its existence.

    That's all there is to it. Snippets from relevant sections of the RoleProvider appear below.

    namespace Company.MOSS.Auth
    {
        public class CustomRoleProvider : RoleProvider
        {
            /*
            TO DO: put your other class variables here
             */

            private bool _LDAPEnableAuthUsersGroup;
            private string _LDAPAuthUsersGroup;

            public CustomRoleProvider()
            {
                /*
                TO DO: set other defaults here
                 */

                this._LDAPEnableAuthUsersGroup = false;
                this._LDAPAuthUsersGroup = string.Empty;
            }

            public override void Initialize(string name, NameValueCollection config)
            {
                this._Name = name;
                if (config != null)
                {
                    try
                    {
                        /*
                        TO DO: initialize your other configuration variables here
                         */

                        //check for the presence of the auth users group configuration setting
                        if (config["authUsersGroup"] != null)
                        {
                            //Extra validation to prevent blank auth users group
                            if (config["authUsersGroup"].Length > 0)
                            {
                                //making this case-insensitive - this might not apply to your directory service
                                this._LDAPAuthUsersGroup = config["authUsersGroup"].ToUpper();
                                //turning on a boolean flag indicating that we're using this feature
                                this._LDAPEnableAuthUsersGroup = true;
                            }
                        }

                        return;
                    }
                    catch (Exception ex)
                    {
                        throw new LdapProviderException(SPResource.GetString("LDAPProviderGeneralFailure",
                            new object[0]));
                    }
                }
                throw new ArgumentNullException("config");
            }

            public override string[] GetRolesForUser(string username)
            {
                //this group list length works for me - might need to be different for you
                List<string> rolesList = new List<string>(10);

                /*
                TO DO: do whatever else you need to do to retrieve the "real" roles first
                 */

                //if you're using the feature, then add the all auth users group to the roles list
                if (_LDAPEnableAuthUsersGroup)
                {
                    rolesList.Add(_LDAPAuthUsersGroup);
                }

                return rolesList.ToArray();
            }

            public override bool RoleExists(string roleName)
            {
                //if authenticated users functionality is enabled, check to see if this is that special group
                if (_LDAPEnableAuthUsersGroup)
                {
                    //making this case-insensitive - this might not apply to your directory service
                    string role = roleName.ToUpper();

                    if (role.Equals(_LDAPAuthUsersGroup)) return true;
                }

                /*
                TO DO: do whatever else you need to do to check if "real" roles exist
                 */
            }

        }
    }

    Special Concerns:

    REALLY, REALLY IMPORTANT: Make sure that the "All Auth Users" group name doesn't intersect with current or possible user/group names. That would be a horrible security hole - if someone created  a user or group with the same name as the All Auth Users group (whatever you choose to call it) and provisioned permissions to it on their site, all of a sudden EVERYONE would have that permission level. Very, very, very bad, potentially with legal/compliance consequences. There are three tactics I recommend for preventing this outcome:

    • Simple, fast solution #1: Create a placeholder group (and user) in the directory service with the same name as the All Auth Users group that YOU own. Never do anything with them other than ensuring that they don't get deleted. You're just putting them there to prevent anybody else from creating a group with the same name, assuming that the directory service enforces uniqueness. :)
    • Simple, fast solution #2: Give the All Auth Users group a name that falls outside the allowable domain of values for group names in your directory service - i.e. include special characters like # or \ if those are disallowed in group names; create an group name 100 characters long if group names are restricted to 50 characters in the directory service; etc.
    • Slow, fallback solution: This could also be used in combination with method #2, but adds a bit of overhead. Instead of the mindless algorithms for checking if the role exists and the list of roles for the user, add a check to see if that role name exists in the directory service. If so, go the safe route and use the "real" non-All Auth users group  instead AND throw a warning to the event log.

    A secondary concern... what if you only want to make this feature available to selected users/sites? This is one of the reasons we went with a configurable setting for the group name - we didn't want your everyday user provisioning permissions to all authenticated users in their site. The design pattern for this is simple - go with an elaborate group name (like a GUID) and change it on a regular basis.

  • Be sure to use paging in membership providers for the MOSS Profile Import Tool...

    Just a quick lesson learned on the MOSS Profile Import Tool to save you import time and resources.

    The Profile Import Tool's current release has a hard-coded paging algorithm that attempts to page through any membership provider's GetAllUsers method by retrieving 1000 users at a time. If you DON'T support paging your GetAllUsers method - meaning you try to retrieve all users at once - then the import algorithm will retrieve and import all users N times, where N = (# users / 1000) . This becomes totally unmanageable - but a really good test for memory leaks in your provider - if you have many thousands of users... :)

    The best practice is (if able) to add paging to your membership provider, since that will be useful in all calls to the GetAllUsers, FindUsersByName, and FindUsersByEmail methods, all of which will be used by SharePoint.

    If you feel the need to alter this aspect of the Profile Import Tool's behavior - i.e. you have 100K users and don't want to take the time to build paging logic into your provider, or don't have access to the membership provider's source code - you'll find the paging logic in the ImportProvider.cs class. All you have to do is change the value of the pageSize constant to a sufficiently large value (i.e. 101000 if you have 100000 users), and you'll bypass the paging:

    private const int pageSize = 1000;

    If you choose to increase the pageSize value, make sure you have enough RAM to hold all those user records... and don't run the profile import application on a server that's hosting a RAM-intensive role like Query, Web App, or SQL.

  • Resolving Issues with Faulty Features and STSADM EXPORT

    If you're attempting an STSADM -o export (or, I'd assume, a content deployment) of a site and receive the following error:

    FatalError: Failed to compare two elements in the array.

    Check out the following KB article: http://support.microsoft.com/kb/948726/en-us .

    The article indicates that the likely cause is one or more invalid feature references in the SPSite and/or SPWeb object. Fortunately, Steven Van de Craen authored a wonderful GUI tool for removing "faulty features" (which is of course unsupported and should be used with caution, but nonetheless very useful). See his article on the topic for more details.

    Cases where you might encounter these errors:

    • Sites that were migrated from a MOSS Enterprise implementation (even if they're not USING Enterprise features) to a MOSS Standard implementation
    • Sites that were created in an beta version of MOSS/WSSv3
    • Sites which reference custom features that have since been removed from the filesystem but were not properly deactivated and uninstalled
  • List Attachments over 50MB need more than an increase in Maximum Upload Size...

    Chalk this down as the 1045th* time I've been reminded that you learn something new about SharePoint every day.

    *The number of days I've been working with SharePoint... :)

    Requirement:

    • Increase the maximum upload size for documents and attachments to 60MB for a web application

    Apparent solution:

    1. Connect to Central Admin
    2. Navigate to Central Admin > Application Management > Web Application General Settings
    3. Select your web application
    4. Set the Maximum Upload Size value to 60 MB and hit OK.

    Results:

    • Upload a 55MB document to a doclib - success.
    • Attach a 55MB file to a list item - receive the classic "An unknown error occurred" message, scratch head in puzzlement

    Turns out there's a setting at the IIS level that will block list attachment uploads above 50MB. Below 50MB, any max upload size you pick (for example, 10MB) works fine for both list attachments and document uploads and is enforced directly through web app settings. Above 50MB, you need to make at least one minor tweak to your web.config to allow larger uploads.

    Complete solution for max upload sizes over 50MB:

    1. Connect to Central Admin
    2. Navigate to Central Admin > Application Management > Web Application General Settings
    3. Select your web application
    4. Set the Maximum Upload Size value to "X" MB and hit OK.
    5. Repeat steps 6-7 for all zones for your web application on all servers hosting the web application role
    6. Open the web.config
    7. Replace the following line:
      <httpRuntime maxRequestLength="51200" />
      with
      <httpRuntime maxRequestLength="{X * 1024}" />

    if you're using SQL storage for your SharePoint content, the maximum useful value for the maxRequestLength should be 2097152 (2GB in kilobytes = 2 * 1024 * 1024). Keep in mind that other limits may affect large uploads, most notably:

    • Site Collection Quota
    • Timeouts (you may need to adjust these in BOTH Web Application General Settings and the executionTimeout attribute of the <httpRuntime/> element)
      • Timeout settings will be influenced by network throughput limitations like latency and available bandwidth
    • Custom Storage Solution
      • External Blob Storage solution (see WSS SDK v1.3 for this feature that's new to WSS SP1) - whatever max BLOB size applies
    • Disk Space
      • If using SQL Storage, this is the disk containing your data files for your webapp's content DBs
      • If using an External Blob Storage solution, then this is wherever your blob store resides

    For more on the httpRuntime web.config element, see http://msdn2.microsoft.com/en-us/library/e1f13641(VS.80).aspx .

  • MOSS User Profile Delete Tool

    For anyone using a custom authentication provider or non AD/LDAP* directory service, the Profile Import tool at http://www.codeplex.com/sptoolbox is an awesome tool for adding users. Just plug in your provider(s), tweak a few configuration settings, and go. I've used it to add 200,000+ user profiles from my customer's directory service (though I wouldn't recommend attempting to swallow all those records in one gulp - that requires more memory than we had on our import server) to our latest MOSS environment.

    Getting the configuration "just right" resulted in my creating lots of extraneous user profiles in the environment. Not wanting to recreate the SSP or build up some serious callouses hitting the "delete" key hundreds of times, I spent quite a bit of time yesterday searching for a tool for doing mass deletes of user profiles, to no avail. I did, however, find a number of postings for other folks out there looking for a similar tool.

    So I put together a quick-and-dirty command-line app for nuking your entire user profile DB or a portion thereof based on account name matches. You could modify this pretty easily to include more sophisticated filter criteria, like an existence check in your directory service of choice, but I didn't want to spend more than an hour on the script... :)

    * You'd also need to use the Profile Import tool if you need to do an authenticated bind to your LDAP service using something other than Windows credentials.

    Configuration File:

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
     <appSettings>
      <add key="test" value="false"/>
      <add key="debug" value="true"/>
      <add key="filterPattern" value="qaldap:"/>
     </appSettings>
    </configuration>

    Configuration Comments:

    Here's how to use the config settings:

    • filterPattern
      • case-insensitive string used for a "starts with" match on user profile Account Names - i.e. "myprovider:" would be a match for all user profiles that use the provider "myprovider", or "mydomain\" would match all user profiles from the "mydomain" domain
    • test - used to control whether delete actions occur
      • true (default) - user profiles matching the filterPattern are reported as matches for deletion, but no delete actions occur
      • false - all user profiles matching the filterPattern are deleted
    • debug
      • true - application info/warning/error events are written to the console
      • false (default) - application events are written only to the event log

    Source Code:

    The code is pretty simple. Main is where the action is for retrieving/deleting user profiles and reading the config file; if you need to plug in a different algorithm for identifying profiles to be deleted (like an existence check in your directory service), you'd add it here. GetAnySiteUrl is copied directly from the Profile Import tool to save coding time in establishing a server context; if you have multiple SSPs, you might want to update the way you establish a context.

    If you're using only 1 SSP and are OK with the simple "starts with" pattern match algorithm, just put the right references in your project, build it, and you're ready to go. Enjoy!

    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Text;
    using System.Diagnostics;
    using System.Configuration;

    using Microsoft.Office.Server;
    using Microsoft.Office.Server.UserProfiles;
    using Microsoft.SharePoint;
    using Microsoft.SharePoint.Administration;

    namespace ProfileDelete
    {
        class Program
        {
            static int logLevel = 1;
            static bool debug = false;
            static bool test = true;
            static string logSource = "ProfileDelete";
            static string logDestination = "Application";
            static string filterPattern = "";

            static void Main(string[] args)
            {
                int counter = 0;

                try
                {
                    Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);

                    NameValueCollection appSettings = ConfigurationManager.AppSettings;
                    logLevel = Convert.ToInt32(appSettings["loglevel"]);
                    debug = Convert.ToBoolean(appSettings["debug"]);
                    test = Convert.ToBoolean(appSettings["test"]);
                    filterPattern = appSettings["filterPattern"];
                }
                catch (Exception e)
                {
                    DoLog("Error reading configuration file:" + e.ToString(), EventLogEntryType.Error, 100);
                }

                string currentAccountName = String.Empty;

                using (SPSite site = new SPSite(GetAnySiteUrl()))
                {

                    ServerContext context = ServerContext.GetContext(site);

                    UserProfileManager profileManager = new UserProfileManager(context);

                    foreach (UserProfile profile in profileManager)
                    {
                        currentAccountName = (String) profile[PropertyConstants.AccountName].Value;
                        currentAccountName = currentAccountName.ToLower();

                        if (currentAccountName.StartsWith(filterPattern.ToLower()))
                        {
                            counter++;
                            DoLog(currentAccountName + " matches delete criteria. Count: " + counter, EventLogEntryType.Information, 110);
                            if (!test)
                            {
                                DoLog("Deleting " + currentAccountName, EventLogEntryType.Information, 120);
                                profileManager.RemoveUserProfile(profile.ID);
                            }
                        }
                    }
                }

            }

            private static string GetAnySiteUrl()
            {
                //the purpose of this function is just to get any site Url so that
                //we can obtain a ServerContext for the UserProfileManager
                string ret = string.Empty;

                SPWebServiceCollection wsc = null;

                try
                {
                    wsc = new SPWebServiceCollection(SPFarm.Local);
                    foreach (SPWebService sw in wsc)
                    {
                        foreach (SPWebApplication wa in sw.WebApplications)
                        {
                            if (wa.Sites.Count > 0)
                            {
                                ret = wa.Sites[0].Url;
                                break;
                            }
                            if (!string.IsNullOrEmpty(ret))
                                break;
                        }
                        if (!string.IsNullOrEmpty(ret))
                            break;
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception("Error getting a site Url: " + ex.Message);
                }
                finally
                {
                    wsc = null;
                }

                return ret;
            }

            private static void DoLog(string msg, EventLogEntryType eventType, int code)
            {
                bool writeEntry = true;
                if (debug) Console.WriteLine("Event Type: {0}; ID: {1}; Message: {2}", eventType.ToString(), code, msg);

                if (logLevel > 0)
                {
                    if (!EventLog.SourceExists(logSource))
                        EventLog.CreateEventSource(logSource, logDestination);
                    if (eventType.Equals(EventLogEntryType.Warning) && (logLevel < 2))
                        writeEntry = false;
                    if (eventType.Equals(EventLogEntryType.Information) && (logLevel < 3))
                        writeEntry = false;
                    if (writeEntry) EventLog.WriteEntry(logSource, msg, eventType, code);
                }
            }


        }
    }

     

  • MOSS+ECM Supplemental Material, Week 3

    Various Topics: 

    • Virtualization:
      • Platform choice:
        • Virtual Server 2005 R2 x86 or x64 is supported
        • Recommended against running production environments on VPC
        • 3rd Party: See KB article 897615
      • Architecture considerations:
        • Recommended NOT to virtualize SQL
        • Restricted to 32-bit SharePoint, so this reduces addressable memory
        • No hard-and-fast rules as to how performance changes when you go from physical to virtual
    • SharePoint 2003-2007 migration webcast - http://msevents.microsoft.com/CUI/WebCastEventDetails.aspx?culture=en-US&EventID=1032330523&CountryCode=US
    • Infopath integration with the BDC - Other than using BDC data in list/library columns in InfoPath forms, you can write a web service to make use of the BDC’s Runtime Object Model:
      The Runtime object model is designed for use by the Business Data Catalog clients and applications. The Runtime object model has two major functions:
      • It offers an intuitive, object-oriented interface that abstracts the underlying data sources. The Runtime object model insulates the client from learning application-specific coding paradigms, and allows clients to access all business applications in a single, simplified way. Because of the Runtime object model, calling a method on an SAP application is very similar to calling a method on Siebel or executing a query in SQL.
      • It allows you to read the metadata objects from the metadata database and execute the business logic described there. The runtime object model is cached and fast, so clients that just need to query the metadata database for metadata information use the Runtime object model.

     

  • ECM Supplemental Material, Week 2

More Posts Next page »

© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker