|
// 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.
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.