Click here to Skip to main content
15,897,704 members
Articles / Web Development / ASP.NET

Stop editing 'web.sitemap' - Let unknown pages dynamically inherit from a parent! And more...

Rate me:
Please Sign up or sign in to vote.
4.80/5 (13 votes)
11 Apr 2009CPOL13 min read 275.6K   1.2K   74  
Reduce sitemap maintenence and never have another "unlisted" page! Unlisted pages dynamically inherit site map placement from a parent page. Replace repetitive '/default.aspx' mentions with '/' for friendlier URLs. Wildcard query string matching and more...
// ScionSiteMapProvider.cs
// ASP.NET 2.0 XmlSiteMapProvider. 
// Tek4.Web.ScionSiteMapProvider assembly
// ==========================================================================
// SCION (pronounced like "cyan"): descendant, child
//
// DESCRIPTION:
// -  Extends System.Web.XmlSiteMapProvider.
// -  Provides site mapping for nodes (URLs) that do not have a site map file
//    entry by inheriting information from parent nodes. This can greatly
//    reduce sitemap file maintenence as a specific entry does not need to be
//    created for each resource!
// -  Provides new "wildcard" query string variable matching.
// -  Provides automatic recognition for the default document (default.aspx)
//    enabling sitemap file entries such as "~/" in place of "~/default.aspx".
// -  Able to strip file extensions (.aspx and others) for web sites using
//    URLs without extensions (see http://www.pagexchanger.com/).
// ==========================================================================
// Copyright 2006-2009 by Kevin P. Rice. All rights reserved.
//
// CONTACT:
// Kevin P. Rice, Tek4 Inventions (http://Tek4.com/)
// PO BOX 14107, SAN LUIS OBISPO CA 93406-4107
//
// Revision History:
// 2009-04-05 KPR - v.1.0.0.3
// - Added System.Security.AllowPartiallyTrustedCallers attribute
// 2008-08-28 KPR
// - Added firing of SiteMap.SiteMapResolve event to CurrentNode method which
//    previously was not implemented, thus breaking the API.
// 2007-02-03 KPR
// - Added security attribute; changed lockObject to readonly.
// 2007-01-14 KPR - v.1.0.0.1
// - Fix: initialization of defaultDocumentList (convert to lowercase)
// - Change: default titleSeparator from "/" to " > "
// 2006-09-12 KPR - v.1.0
// 2006-08-27 KPR - Created. v.0.9

using System;
using System.Reflection;
using System.Security;
using System.Web;

[assembly: AllowPartiallyTrustedCallers]
[assembly: AssemblyTitle("Tek4.Web.ScionSiteMapProvider")]
[assembly: AssemblyDescription("ASP.NET 2.0 SiteMapProvider. " + 
  "Extends XmlSiteMapProvider to provide mapping for nodes that do not" +
  "have a site map entry by inheriting information from a parent node.")]
[assembly: AssemblyCompany("Tek4 Innovations (http://Tek4.com/)")]
[assembly: AssemblyProduct("Tek4.Web.ScionSiteMapProvider")]
[assembly: AssemblyCopyright("Copyright 2006-2009 Kevin P. Rice. " + 
  "All rights reserved.")]
[assembly: AssemblyVersion("1.0.0.3")] // major, minor, build, revision

[assembly: System.CLSCompliant(true)] // enforce CLS compliance

namespace Tek4.Web.ScionSiteMapProvider {

  /// <summary>
  /// ScionSiteMapProvider. Extends XmlSiteMapProvider to provide mapping 
  /// for URLs that do not have a sitemap file entry by inheriting
  /// information from a parent entry.
  /// </summary>
  [AspNetHostingPermission(
    System.Security.Permissions.SecurityAction.Demand, 
    Level = AspNetHostingPermissionLevel.Minimal),
  AspNetHostingPermission(
		System.Security.Permissions.SecurityAction.InheritanceDemand, 
		Level = AspNetHostingPermissionLevel.Minimal)]
  public class ScionSiteMapProvider : XmlSiteMapProvider {

    /// <summary>
    /// Defines the naming methods for scion node titles.
    /// </summary>
    public enum TitleStyle {
      Path,     // full sub-path from URL
      First,    // first (highest) level name from URL only
      Last      // last (lowest) level name from URL only
    }

    private const string attribDefaultDoc = "defaultDocuments";
    private const string attribDescription = "description";
    private const string attribInherit = "inherit";
    private const string attribScionTitle = "scionTitle";
    private const string attribStripFileExt = "stripFileExt";
    private const string attribTitleSeparator = "titleSeparator";
    private const string defaultDocuments = "default.aspx";

    private const char delimDocList = '|';
    private const char delimFileExt = '.';
    private const char delimUrlLevel = '/';
    private const char delimUrlVars = '?';

    private static readonly Object lockObject = 
			new Object(); // used for thread locking

    private System.Collections.ArrayList defaultDocumentList;
    private bool inheritanceEnabled = true;
    private TitleStyle scionTitle = TitleStyle.Path;
    private string scionSiteMapProviderDescription =
      "Extends XmlSiteMapProvider to provide mapping for URLs " +
      "that do not have a sitemap file entry by inheriting " +
      "information from a parent entry.";
    private string scionSiteMapProviderName = "Tek4ScionSiteMapProvider";
    private bool stripFileExt = false;
    private string titleSeparator = " > ";

    /// <summary>
    /// Gets or sets the list of default document file names. The default
    /// list is { "default.aspx" }.
    /// </summary>
    public virtual System.Collections.ArrayList DefaultDocuments {
      get { return defaultDocumentList; }
      set { defaultDocumentList = value; }
    }

    /// <summary>
    /// Gets a brief, friendly description suitable for display in
    /// administrative tools or other user interfaces (UIs). 
    /// </summary>
    public override string Description {
      get { return scionSiteMapProviderDescription; }
    }

    /// <summary>
    /// Gets or sets whether or not site map node inheritance is enabled.
    /// The default value is true.
    /// </summary>
    public virtual bool InheritanceEnabled {
      get { return inheritanceEnabled; }
      set { inheritanceEnabled = value; }
    }

    /// <summary>
    /// Gets the friendly name used to refer to the provider during
    /// configuration.
    /// The recommended pattern for this string is:
    /// [Provider Creator][Implementation Type][Feature]Provider.
    /// </summary>
    public override string Name {
      get { return scionSiteMapProviderName; }
    }

    /// <summary>
    /// Gets the TitleStyle used for scion nodes.
    /// </summary>
    public virtual TitleStyle ScionTitle {
      get { return scionTitle; }
      set { scionTitle = value; }
    }

    /// <summary>
    /// Gets or sets whether or not URL file extensions are stripped.
    /// The default value is false.
    /// </summary>
    public virtual bool StripFileExt {
      get { return stripFileExt; }
      set { stripFileExt = value; }
    }

    /// <summary>
    /// Gets the string used as a separator between path levels when
    /// ScionTitle is set to TitleStyle.Path.
    /// </summary>
    public virtual string TitleSeparator {
      get { return titleSeparator; }
      set { titleSeparator = value; }
    }

    /// <summary>
    /// Initializes the ScionSiteMapProvider object. The Initialize() method
    /// does not actually build a site map, it only prepares the state of the
    /// ScionSiteMapProvider to do so.
    /// </summary>
    /// <param name="name">The ScionSiteMapProvider to initialize.</param>
    /// <param name="attributes">
    /// A NameValueCollection that can contain additional attributes to help
    /// initialize name. These attributes are read from the XmlSiteMapProvider
    /// configuration in the Web.config file. 
    /// </param>
    public override void Initialize(string name, 
      System.Collections.Specialized.NameValueCollection attributes) {
      lock (lockObject) {

        // get name from web.config attributes
        if (!String.IsNullOrEmpty(name))
          scionSiteMapProviderName = name;

        // get config information from web.config

        string val;

        // description attribute
        val = attributes[attribDescription];
        if (String.IsNullOrEmpty(val)) {
          attributes.Remove(attribDescription);
          attributes.Add(attribDescription, scionSiteMapProviderDescription);
        } else
          scionSiteMapProviderDescription = attributes[attribDescription];

        // defaultDocuments attribute
        val = attributes[attribDefaultDoc];
        char[] delimiter = { delimDocList };
        if (val != null) {
          attributes.Remove(attribDefaultDoc);
          defaultDocumentList = new System.Collections.ArrayList(
            val.ToLowerInvariant().Split(
						delimiter, StringSplitOptions.RemoveEmptyEntries));
        } else
          defaultDocumentList = new System.Collections.ArrayList(
            defaultDocuments.Split(delimiter));

        // inherit attribute
        val = attributes[attribInherit];
        if (val != null) {
          attributes.Remove(attribInherit);
          inheritanceEnabled = Convert.ToBoolean(val);
        }

        // scionTitle attribute
        val = attributes[attribScionTitle];
        if (val != null) {
          attributes.Remove(attribScionTitle);
          scionTitle = (TitleStyle)Enum.Parse(typeof(TitleStyle), val, true);
        }

        // stripFileExt attribute
        val = attributes[attribStripFileExt];
        if (val != null) {
          attributes.Remove(attribStripFileExt);
          stripFileExt = Convert.ToBoolean(val);
        }

        // titleSeparator attribute
        val = attributes[attribTitleSeparator];
        if (val != null) {
          attributes.Remove(attribTitleSeparator);
          titleSeparator = val;
        }

        // initialize the base class
				base.Initialize(scionSiteMapProviderName, attributes);

      } // lock
    }
    
    /// <summary>
    /// Gets the SiteMapNode object that represents the currently 
    /// requested page.
    /// </summary>
    public override SiteMapNode CurrentNode {
      get {
        SiteMapNode node;   // node returned

        // obtain HttpContext object for current request
        HttpContext context = HttpContext.Current;
        if (context == null) return null;

        // attempt to resolve the node using the base class
        node = ResolveSiteMapNode(context);
        if (node != null) return node;

        // obtain raw URL
        string rawUrl = context.Request.CurrentExecutionFilePath;

        try {
          System.Threading.Monitor.Enter(lockObject);

          // this should never occur, but we'll test upfront!
          if (rawUrl.IndexOf(delimUrlLevel) != 0)
            throw new ArgumentException("URL must start with '/'.");

          SiteMapNode parent; // parent node of scion

          string baseUrl;       // base URL (w/o default doc. or vars.)
          string name;          // URL hierarchal-level name (from path)
          string path = rawUrl; // working URL path (gets trimmed later)
          string vars;          // URL query string variables

          int idxLast;  // index for last URL hierarchal-level '/' delimiter
          int idxVars;  // index for start of query string variables

          bool isDefaultDocument; // set if default doc (e.g., default.aspx)

          // strip any query string vars off the URL
          idxVars = path.IndexOf(delimUrlVars);  // index of URL vars
          if (idxVars != -1) {
            vars = path.Substring(idxVars);
            path = path.Remove(idxVars);
          } else vars = string.Empty;

          // remove the file name from the URL
          // (one better exist or we'll throw an exception here!)
          idxLast = path.LastIndexOf(delimUrlLevel);
          name = path.Substring(idxLast + 1);
          path = path.Remove(idxLast + 1);

          // if request is for the default document, clear the name string
          isDefaultDocument = IsDefaultDocument(name, path);
          if (isDefaultDocument) name = string.Empty;

          // ...else if file extension stripping enabled then remove extension
          else if (stripFileExt) {
            int idxExt = name.LastIndexOf(delimFileExt);
            if (idxExt != -1) name = name.Remove(idxExt);
          }

          baseUrl = path + name; // build base URL from cleaned-up parts

          // ==========================
          // look for exact URL matches first

          // attempt exact URL match (with query string variables)
          node = FindSiteMapNode(baseUrl + vars);
          if (node != null)
            return node.IsAccessibleToUser(context) ? node : null;

          // Deal with any query string variables:
          // (1) Strip variables leaving '?' and attempt match (wildcard case)
          // (2) Strip '?', attempt match
          if (idxVars != -1) {
            node = FindSiteMapNode(baseUrl + delimUrlVars);
            if (node == null) node = FindSiteMapNode(baseUrl);
            if (node != null)
              return node.IsAccessibleToUser(context) ? node : null;
          }

          if (!inheritanceEnabled) return null; // inheritance disabled?

          // ===================================================
          // No exact match found -- attempt to find an ancestor

          // if not default document, then current path was not checked yet
          if (!isDefaultDocument) {
            if (!stripFileExt) { // strip file extension if not done yet
              int idxExt = name.LastIndexOf(delimFileExt);
              if (idxExt != -1) name = name.Remove(idxExt);
            }
            parent = FindSiteMapNode(path);
            if (parent != null) {
              if (!parent.IsAccessibleToUser(context)) return null;
              node = new SiteMapNode(this, baseUrl,
                rawUrl, name, parent.Description); // build new scion node
              AddNode(node, parent);  // add node to provider collection
              return node;  // return scion node
            }
          }

          // get hierarchal level name from URL
          path = path.Remove(idxLast); // remove trailing '/'
          idxLast = path.LastIndexOf(delimUrlLevel); // find last '/'

          // iteratively remove hierarchal-level names from URL path until
          // application root is reached attempting to match at each level
          while (idxLast != -1) {
            switch (scionTitle) { // build scion node name from URL
              case TitleStyle.Path:
                name = name.Length == 0 ?
                  path.Substring(idxLast + 1) :
                  path.Substring(idxLast + 1) + titleSeparator + name;
                break;
              case TitleStyle.First:
                name = path.Substring(idxLast + 1);
                break;
              case TitleStyle.Last:
                if (name.Length == 0) name = path.Substring(idxLast + 1);
                break;
            }
            path = path.Remove(idxLast + 1);    // remove hierarchal name
            parent = FindSiteMapNode(path);     // attempt match
            if (parent != null) {               // match found
              if (!parent.IsAccessibleToUser(context)) return null;
              node = new SiteMapNode(this, baseUrl,
                rawUrl, name, parent.Description);  // build new scion node
              AddNode(node, parent);  // add node to provider collection
              return node;  // return scion node
            }
            path = path.Remove(idxLast);        // remove trailing '/'
            idxLast = path.LastIndexOf(delimUrlLevel); // find last '/'
          }

        } catch (ArgumentException e) {

          // Test for invalid URLs
          // We don't believe that the following URLs could ever be passed
          // on. These will cause exceptions somewhere above.
          // (1) empty
          // (2) does not begin with '/' (not absolute to application root)
          // (3) ends with '/' (doesn't specify a file resource)
          if (rawUrl.IndexOf(delimUrlLevel) != 0 ||
            rawUrl.EndsWith(Convert.ToString(delimUrlLevel)) ||
            rawUrl.EndsWith(Convert.ToString(delimUrlVars))) {
            throw new ArgumentException(
              "URL must start with '/' and must not end with '/'. " +
              "URL passed was '" + HttpContext.Current.Request.RawUrl + "'.",
              "HttpContext.Current.Request.RawUrl", e);
          }

          throw e;  // re-throw anything else
        
        } finally {
          System.Threading.Monitor.Exit(lockObject);
        }

        return null; // no ancestor found
      }
    }

    /// <summary>
    /// Returns true if the specified file is a default document for the
    /// specified application directory.
    /// </summary>
    /// <param name="name">A file name from an application directory.</param>
    /// <param name="dir">A web application directory.</param>
    /// <returns>True if name is the default resource.</returns>
    protected virtual bool IsDefaultDocument(String name, String dir) {

      /////////////////////////////////
      // This method could be extended to automatically search the IIS
      // metabase to determine default documents for the given directory!

      return defaultDocumentList.Contains(name.ToLowerInvariant());
    }

  } // class ScionSiteMapProvider

} // namespace

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Kevin Rice is a firefighter for a major Southern California fire department. When not working, he enjoys web programming, administration, riding dirt-bikes (Visit SLORider.com) or building anything electronic.

Comments and Discussions