using System;
using System.Drawing;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.Design;
using System.ComponentModel;
using System.Text;
using System.Collections;
using CP.WebControls;
namespace CP.WebControls
{
/// <summary>
/// Control to create a mailto: link with an encoded email
/// address which will pass any email validation routine
/// </summary>
[
DefaultProperty("Email"),
ToolboxData("<{0}:NoSpamEmailHyperlink runat=server></{0}:NoSpamEmailHyperlink>"),
ParseChildren(false),
ControlBuilder(typeof(NoSpamEmailHyperlinkBuilder)),
Designer(typeof(NoSpamEmailHyperlinkDesigner)),
DataBindingHandler(typeof(NoSpamEmailHyperlinkDataBindingHandler))
]
public class NoSpamEmailHyperlink : System.Web.UI.WebControls.WebControl
{
/// <summary>
/// Tag is A (hyperlink) rather than the default SPAN
/// </summary>
protected override HtmlTextWriterTag TagKey
{
get
{
return HtmlTextWriterTag.A;
}
}
#region Text property (inner property)
/// <summary>
/// Text to display for link
/// </summary>
[
Bindable(true),
Category("Data"),
DefaultValue(""),
Description("Display text for hyperlink (may also be encoded)"),
PersistenceMode(PersistenceMode.EncodedInnerDefaultProperty)
]
public virtual string Text
{
get
{
return (ViewState["Text"] is string)
? (string) ViewState["Text"]
: String.Empty;
}
set
{
ViewState["Text"] = value;
}
}
/// <summary>
/// Handle the literal content of the control
/// </summary>
/// <param name="obj">Child control</param>
protected override void AddParsedSubObject(object obj)
{
if (obj is LiteralControl)
{
// If inner text is a literal, we can
// pick up the text at any point and
// populate our Text property
Text = ((LiteralControl)obj).Text;
}
else
{
// If inner text is databound, we
// need to pick up the text at render
// time, so we'll add the control as
// a child
base.AddParsedSubObject(obj);
}
// NoSpamEmailHyperlinkBuilder.AppendSubBuilder
// will throw out any other child controls at
// parse-time, no other condition should exist
}
#endregion
#region Attribute properties
/// <summary>
/// Email address to be encoded
/// </summary>
[
Bindable(true),
Category("Data"),
DefaultValue(""),
Description("Email address for hyperlink")
]
public virtual string Email
{
get
{
return (ViewState["Email"] is string)
? (string) ViewState["Email"]
: String.Empty;
}
set
{
ViewState["Email"] = value;
}
}
/// <summary>
/// Seed number for decoding script
/// </summary>
[
Bindable(true),
Category("Data"),
DefaultValue(23),
Description("Seed number for decoding script")
]
public virtual int ScrambleSeed
{
get
{
return (ViewState["ScrambleSeed"] is int)
? (int) ViewState["ScrambleSeed"]
: 23;
}
set
{
ViewState["ScrambleSeed"] = value;
}
}
/// <summary>
/// Set to false to avoid encoding where the email
/// address is in the inner text
/// </summary>
[
Bindable(true),
Category("Data"),
DefaultValue(true),
Description("If the email address appears in the Text property, encode it")
]
public virtual bool EncodeInText
{
get
{
return (ViewState["EncodeInText"] is bool)
? (bool) ViewState["EncodeInText"]
: true;
}
set
{
ViewState["EncodeInText"] = value;
}
}
/// <summary>
/// Text to replace email address in the Text for Netscape pre-v6
/// </summary>
[
Bindable(true),
Category("Data"),
DefaultValue("[Hidden]"),
Description("Text to replace email address in the Text for Netscape pre-v6")
]
public virtual string HideText
{
get
{
return (ViewState["HideText"] is string)
? (string) ViewState["HideText"]
: "[Hidden]";
}
set
{
ViewState["HideText"] = value;
}
}
#endregion
#region Hidden properties for inheritors
/// <summary>
/// A base string for scrambling text
/// </summary>
/// <remarks>
/// This should contain as many alpha-numeric characters
/// as possible but each character should only appear once.
/// Avoid using punctuation ('@', '.', '_', '-') which can
/// make the address invalid.
/// </remarks>
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
protected virtual string CodeKey
{
get
{
return "yJzdeB4CcDnmEFbZtvuHlI1hA8SiLo9MwfN3O6Y5QaRqKTjUpxVk2WgXrP7Gs0";
}
}
/// <summary>
/// Unique name for registering Link Array
/// </summary>
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
protected virtual string LinkArrayName
{
get
{
return GetType().Name + "_LinkNames";
}
}
/// <summary>
/// Unique name for registering Seed Array
/// </summary>
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
protected virtual string SeedArrayName
{
get
{
return GetType().Name + "_Seeded";
}
}
/// <summary>
/// Unique name for registering function script
/// </summary>
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
protected virtual string FuncScriptName
{
get
{
return GetType().Name + "_DecodeScript";
}
}
/// <summary>
/// Unique name for registering call script
/// </summary>
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
protected virtual string CallScriptName
{
get
{
return GetType().Name + "_DecodeScriptCall";
}
}
/// <summary>
/// Variable name used for CodeKey string
/// </summary>
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
protected virtual string CodeKeyName
{
get
{
return "ky";
}
}
/// <summary>
/// If a broswer cannot set the innerHTML, this should identify it
/// </summary>
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
protected virtual bool BrowserNeedsHide
{
get
{
// If the Browser is Netscape (v4.x or less), we cannot change
// the innerHTML at run time, so we'll just hide it
HttpBrowserCapabilities bc = Page.Request.Browser;
Version bv = new Version(bc.Version);
return (bc.Browser.ToLower().IndexOf("netscape") > -1
&& bv.Major < 5);
}
}
#endregion
#region Script building methods
/// <summary>
/// Build the decoder function
/// </summary>
protected virtual string GetFuncScript()
{
#if DEBUG
// Formatted script text in debug version
JavaScriptBuilder jsb = new JavaScriptBuilder(true);
#else
// Compress script text in release version
JavaScriptBuilder jsb = new JavaScriptBuilder();
#endif
jsb.AddLine("function ", FuncScriptName, "(link, seed)");
jsb.OpenBlock(); // function()
jsb.AddCommentLine("This is the decoding key for all ", LinkArrayName, " objects");
jsb.AddLine("var ", CodeKeyName, " = \"", CodeKey, "\";");
jsb.AddLine();
if (!BrowserNeedsHide)
{
jsb.AddCommentLine("Store the innerHTML so that it doesn't get");
jsb.AddCommentLine("distorted when updating the href later");
jsb.AddLine("var storeText = link.innerHTML;");
jsb.AddLine();
}
jsb.AddCommentLine("Initialize variables");
jsb.AddLine("var baseNum = parseInt(seed);");
jsb.AddLine("var atSym = link.href.indexOf(\"@\");");
jsb.AddLine("if (atSym == -1) atSym = 0;");
jsb.AddLine("var dotidx = link.href.indexOf(\".\", atSym);");
jsb.AddLine("if (dotidx == -1) dotidx = link.href.length;");
jsb.AddLine("var scramble = link.href.substring(7, dotidx);");
jsb.AddLine("var unscramble = \"\";");
jsb.AddLine("var su = true;");
jsb.AddLine();
jsb.AddCommentLine("Go through the scrambled section of the address");
jsb.AddLine("for (i=0; i < scramble.length; i++)");
jsb.OpenBlock(); // for (i = 0; i < scramble.length; i++)
jsb.AddCommentLine("Find each character in the scramble key string");
jsb.AddLine("var ch = scramble.substring(i,i + 1);");
jsb.AddLine("var idx = ", CodeKeyName, ".indexOf(ch);");
jsb.AddLine();
jsb.AddCommentLine("If it isn't there then add the character");
jsb.AddCommentLine("directly to the unscrambled email address");
jsb.AddLine("if (idx < 0)");
jsb.OpenBlock(); // if (idx < 0)
jsb.AddLine("unscramble = unscramble + ch;");
jsb.AddLine("continue;");
jsb.CloseBlock(); // if (idx < 0)
jsb.AddLine();
jsb.AddCommentLine("Decode the character");
jsb.AddLine("idx -= (su ? -baseNum : baseNum);");
jsb.AddLine("baseNum -= (su ? -i : i);");
jsb.AddLine("while (idx < 0) idx += ", CodeKeyName, ".length;");
jsb.AddLine("idx %= ", CodeKeyName, ".length;");
jsb.AddLine();
jsb.AddCommentLine("... and add it to the unscrambled email address");
jsb.AddLine("unscramble = unscramble + ", CodeKeyName, ".substring(idx,idx + 1);");
jsb.AddLine("su = !su;");
jsb.CloseBlock(); // for (i = 0; i < scramble.length; i++)
jsb.AddLine();
jsb.AddCommentLine("Adjust the href property of the link");
jsb.AddLine("var emAdd = unscramble + link.href.substring(dotidx, link.href.length + 1);");
jsb.AddLine("link.href = \"mailto:\" + emAdd;");
jsb.AddLine();
if (!BrowserNeedsHide)
{
jsb.AddCommentLine("If the scrambled email address is also in the text");
jsb.AddCommentLine("of the hyperlink, replace it");
jsb.AddLine("var findEm = storeText.indexOf(scramble);");
jsb.AddLine("while (findEm > -1)");
jsb.OpenBlock(); // while (findEm > -1)
jsb.AddLine("storeText = storeText.substring(0, findEm) + emAdd + storeText.substring(findEm + emAdd.length, storeText.length);");
jsb.AddLine("findEm = storeText.indexOf(scramble);");
jsb.CloseBlock(); // while (findEm > -1)
jsb.AddLine();
jsb.AddLine("link.innerHTML = storeText;");
}
jsb.CloseBlock(); // function()
return jsb.ToString();
}
/// <summary>
/// Build the call script text
/// </summary>
/// <remarks>
/// Use the CallScriptName property to create variable names,
/// so that we do not clash with similar (or even completely
/// different) controls.
/// </remarks>
[
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
protected string GetCallScript()
{
#if DEBUG
// Formatted script text in debug version
JavaScriptBuilder jsb = new JavaScriptBuilder(true);
#else
// Compress script text in release version
JavaScriptBuilder jsb = new JavaScriptBuilder();
#endif
jsb.AddCommentLine("Run through all links in this page");
if (Page.Request.Browser.Browser.IndexOf("ie") > -1)
{
jsb.AddLine("for (", CallScriptName, "_idx = 0; ", CallScriptName, "_idx < ", LinkArrayName, ".length; ", CallScriptName, "_idx++)");
jsb.OpenBlock();
jsb.AddLine(FuncScriptName, "(document.links.item(", LinkArrayName, "[", CallScriptName, "_idx]), ", SeedArrayName, "[", CallScriptName,"_idx]);");
jsb.CloseBlock();
}
else
{
jsb.AddLine("for (", CallScriptName, "_idx = 0; ", CallScriptName, "_idx < document.links.length; ", CallScriptName, "_idx++)");
jsb.OpenBlock();
jsb.AddLine("for (", FuncScriptName, "_idx = 0; ", FuncScriptName, "_idx < ", LinkArrayName, ".length; ", FuncScriptName, "_idx++)");
jsb.OpenBlock();
jsb.AddLine("if (document.links[", CallScriptName, "_idx].id == ", LinkArrayName, "[", FuncScriptName, "_idx])");
jsb.OpenBlock();
jsb.AddLine(FuncScriptName, "(document.links[", CallScriptName, "_idx], ", SeedArrayName, "[", FuncScriptName,"_idx]);");
jsb.CloseBlock();
jsb.CloseBlock();
jsb.CloseBlock();
}
return jsb.ToString();
}
#endregion
#region Encoding Algorithm
protected string Encode (string Unencoded, string ToEncode)
{
// Cannot String.Replace a zero-length string
if (ToEncode.Length == 0) return Unencoded;
// Encode all occurences of ToEncode in Unencoded
return Unencoded.Replace(ToEncode, Encode(ToEncode));
}
protected virtual string Encode (string Unencoded)
{
// Convert string to char[]
char[] scramble = Email.ToCharArray();
// Initialize variables
int baseNum = ScrambleSeed;
bool subtract = true;
// Find the @ symbol and the following .
// if either don't exist then we don't have a
// valid email address and should return it unencoded
int atSymbol = Array.IndexOf(scramble, '@');
if (atSymbol == -1) atSymbol = 0;
int stopAt = Array.IndexOf(scramble, '.', atSymbol);
if (stopAt == -1) stopAt = scramble.Length;
// Go through the section of the address to be scrambled
for (int i=0; i < stopAt; i++)
{
// Find each character in the scramble key string
char ch = scramble[i];
int idx = CodeKey.IndexOf(ch);
// If it isn't there then ignore the character
if (idx < 0) continue;
// Encode the character
idx += (subtract ? -baseNum : baseNum);
baseNum -= (subtract ? -i : i);
while (idx < 0) idx += CodeKey.Length;
idx %= CodeKey.Length;
scramble[i] = CodeKey[idx];
subtract = !subtract;
}
// Return the encoded string
return new string(scramble);
}
#endregion
#region Rendering functionality
/// <summary>
/// Add the href atribute to the main tag
/// </summary>
/// <param name="writer">The HtmlTextWriter for the page</param>
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
if (Email.Length > 0)
{
writer.AddAttribute(HtmlTextWriterAttribute.Href, "mailto:" + Encode(Email));
}
base.AddAttributesToRender (writer);
}
/// <summary>
/// Declare arrays and scripts before rendering
/// </summary>
/// <param name="e">EventArgs</param>
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender (e);
if (Email.Length > 0)
{
// Register the Control's ID and Decode seed in scripted arrays
Page.RegisterArrayDeclaration(
LinkArrayName, String.Format("\"{0}\"", ClientID)
);
Page.RegisterArrayDeclaration(
SeedArrayName, String.Format("\"{0}\"", ScrambleSeed)
);
// Register the decoder function script block
if (!Page.IsClientScriptBlockRegistered(FuncScriptName))
Page.RegisterClientScriptBlock(FuncScriptName, GetFuncScript());
// Register the calling script block
if (!Page.IsStartupScriptRegistered(CallScriptName))
Page.RegisterStartupScript(CallScriptName, GetCallScript());
}
}
/// <summary>
/// Render this control to the output parameter specified.
/// </summary>
/// <param name="output">The HtmlTextWriter for the page</param>
protected override void Render(HtmlTextWriter output)
{
// If we don't have anything to display don't even render
// the start and end blocks. This assures that the page shows
// nothing but the VS.NET designer has a selectable label
if (Email.Length == 0 && Text.Length == 0 && Controls.Count == 0)
return;
base.Render(output);
}
protected override void RenderContents(HtmlTextWriter writer)
{
// The only controls that should be left at this
// point should be DataBoundLiteralControls. In theory,
// if there are any then there should be exactly one
// and nothing else. But we'll leave in a bit of
// flexibility, in case the framework changes later.
string displayText = null;
if (Controls.Count > 0 && Controls[0] is DataBoundLiteralControl)
{
displayText = ((DataBoundLiteralControl)Controls[0]).Text;
}
else
{
// If there is some text, use it. If not then display the
// encoded email address or the hyperlink will not be
// visible
displayText = (Text.Length == 0)
? Email
: Text;
}
// If the EncodeInText flag is set and the email address
// is somewhere in the Text, encode it.
if (EncodeInText && Email.Length > 0)
{
int idx = displayText.IndexOf(Email);
if (idx > -1) displayText = BrowserNeedsHide ? HideText : Encode(displayText, Email);
}
// Html encode any text that remains.
writer.Write(HttpUtility.HtmlEncode(displayText));
}
#endregion
}
}