Introduction
Nowadays lots of web applications such as forums and blogs use HTML editors as a primary tool for users publications. Web based HTML editor is a kind of control that allows online users to create and edit their html documents in a way that user can write text and set font, color, size, and hyperlink for it and also insert image,swf, files in that. Furthermore the user can view the HTML code of his document and edit it and view the result in design mode and vice versa.
This article discusses how can create an HTML editor server control with some security considerations.
Background
Four years ago I was working in a company which was so cautious about developing secure products, thus our technical manager asked me about developing an HTML text editor as a module in our CMS Portal for our important customers which security was their first priority. So I developed an HTML editor which considered some important security issues such as XSS. Generally it is not more secure than Current new versions of text editors like FCKEditor but it was a good opportunity for me to create a useful, user friendly and above all a unique experience for my mind necessity for programming some creative web-based software. I hope this article helps you too.
System Requirements
UI
Component appearance:
- Three toolbar rows across the top
- Dialog bars for adding elements like table,button, etcetera.
- An editor area that displays and/or edits the document in either mode
Component Parts
There are two part for this custom control, server side and client side.server side undertakes some tasks such as security stuffs using AntiXSS, providing the html source of html editor,setting leveles of security,providing some methods like setting value for editor and getting secure value from editor,script registration,storing uploaded files, providing decoded data which has been sent from html editor and providing WebResources for control.
The client side guarantees eliminating some dangerous stuffs(XSS aspects) from html editor source which will send to the server side and encoding them, sending data to the serve side , etc.
In client side there are six JavaScript source files
- RichText.js is the main javascript source , all things which is related with editor abilities is done here.
- Encoder.js provides all methods for encoding data
- Loading.js provides optional loader for editor.(some part of it has been commented, if you want to have loader,uncomment them)
- Slider.js provides a slider for resizing the editor text area
- jscolor.js is an open source javascript which i just used it
In server side there are five classes included:
- HtmlSourceInitializer.cs : Initializes the editor HTML
- Registrar.cs : Registers a list of scripts into our control
- Security.cs : Responsibles for security using AntiXSS AND HtmlSanitizationLibrary
- SourceActions.cs : Get the data which has been sent from cleint
- RichTextBox.cs : Establishes accessible methods and properties of HtmlEditor control
EditorStyles , RichTextBoxIcons , Colorpicker are another parts.
HtmlSourceInitializer.cs
The component has some its own HTML sources which creates HTML editor view.This class uses StringBuilder to append HTML Editor view parts which has been added in_HtmlSource
with InitializeHtmlSource
method and then places the _HtmlSource
into the Static RichTextHtmlSource
property.
internal static class HtmlSourceInitializer
{
#region fields
private static StringBuilder _HtmlSource = null;
#endregion
#region getHtmlSource
public static StringBuilder RichTextHtmlSource
{
get
{
if (_HtmlSource != null)
{
return _HtmlSource;
}
return null;
}
}
#endregion
#region Initialize html source
public static void InitializeHtmlSource(Page CurrentPage)
{
StringBuilder HtmlSource = new StringBuilder();
HtmlSource.Append("<center id=\"centerElement\" style=\"display:none\">");
...............
Security.cs
For preventing cross-site scripting,i decided to use Microsoft AntiXSS library. As you can read in its overview :
"The Microsoft Anti-Cross Site Scripting Library V4.2 (AntiXSS V4.2) is an encoding library designed to help developers protect their ASP.NET web-based applications from XSS attacks. It differs from most encoding libraries in that it uses the white-listing technique -- sometimes referred to as the principle of inclusions -- to provide protection against XSS attacks.
This approach works by first defining a valid or allowable set of characters, and encodes anything outside this set (invalid characters or potential attacks). The white-listing approach provides several advantages over other encoding schemes."
The _SetHighLevelSecurityForHtmlTags
field, has been setted as True
which means the html document which has been sent from client side,will decode in two levels and it will change to only text codes. For instance the "<b>hello</b>" will be something like this ";<b>hello</b>".It is useful in some cases which you want to get much more secured html code and after checking it,you can decode it to get a pure HTML source again.
The EncodeHtml
method encodes the string HTML source of created document AntiXSS HtmlEncode
method. Also the GetSafeHtmlFragment
method returns HTML fragments with tags intact.
public static class Security
{
internal static bool _SetHighLevelSecurityForHtmlTags = true;
internal static string EncodeHtml(string html)
{
return AntiXss.HtmlEncode(Sanitizer.GetSafeHtmlFragment(html));
}
}
SourceActions.cs
The SourceActions
class provides the HTML source code which has been sent from client with SourceProvider
.
public class SourceActions : System.Web.UI.Page
{
#region fields
internal string _SourceCode = string.Empty;
#endregion
#region Cunstructor
public SourceActions()
{
}
#endregion Cunstructor
#region Source provider
internal void SourceProvider(Page CurrentPage)
{
CurrentPage.ClientScript.GetPostBackEventReference(CurrentPage, string.Empty);
if (CurrentPage.IsPostBack)
{
string eventTarget = (CurrentPage.Request["__EVENTTARGET"] == null ?
string.Empty : CurrentPage.Request["__EVENTTARGET"]);
string eventArgument = (CurrentPage.Request["__EVENTARGUMENT"] == null ?
string.Empty : CurrentPage.Request["__EVENTARGUMENT"]);
if (eventTarget == "getHtmlData")
{
if (SigmaToolBox.TextEditor.Security._SetHighLevelSecurityForHtmlTags)
{
_SourceCode = SigmaToolBox.TextEditor.Security.EncodeHtml(eventArgument);
}
else
{
_SourceCode = eventArgument;
}
}
}
}
#endregion
}
RichTextBox.cs
SetValue method
This method will set some values like HTML document for editor. For instant if you want to use some documents which have been stored in database or other resources, you can use this method to insert your documents into the editor for edition.
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public void SetValue(string Value)
{
string script = "disableElement(document.getElementById('textToolsContainer'));" +
"document.getElementById('textEditor').style.display='none';isOnSourceMode" +
"=true;isOndesignMode=false;document.getElementById('sourceTxt')." +
"style.display='block';document.getElementById('sourceTxt').value='" + Value + "'";
Page.ClientScript.RegisterStartupScript(Page.GetType(),
"valueSetterScript", script, true);
}
GetValue method
This function gets the data which has been provided in text editor for some purposes such as : demonstration in a specified page,storing in database,etc.
public string GetValue()
{
return this.Page.Server.HtmlDecode(AntiXss.HtmlAttributeEncode(GetDecodedValue()));
}
OnPreRender
must be modified to call RegisterStartupScript
, as follows:
protected override void OnPreRender(EventArgs e)
{
base.OnInit(e);
Registrar.RegisterScripts(new List<string> { "SigmaToolBox.js.loading.js",
"SigmaToolBox.js.testJS.js", "SigmaToolBox.js.Encoder.js",
"SigmaToolBox.js.slider.js" }, this.Page, this.GetType());
string InitializerJS = Page.ClientScript.GetWebResourceUrl(this.GetType(),
"SigmaToolBox.js.Initializer.js");
this.Page.ClientScript.RegisterStartupScript(this.GetType(),
"RichText", "<script language="\""javascript\" src='" +
InitializerJS + "'></script>");
}
RichText.js
Because the client-side behaviors for an HTML editor are fairly extensive, there is a fairly extensive amount of JavaScript required in the client control. Most of this is typical designMode
client-side programming.
This includes some significant functions which undertake some tasks such as :
- Commands execution on editor
- Elements creation
- All other design mode activities
- Making handlers for handeling events
The document is created and design with these javascript functions and then if someone wants to use it on server side (for example: storing/decoding)should send it to the server.This action happens with sendValue().
sendValue()
function send created document to the server,as well as it eliminates some of the risky tags over XSS attacks like script
,iframe
,javascript
and encode the document using Encoder.js<code>(with htmlEncode() function)
before sending it to the server. Actually there will be two step of encoding process,first step in the client and the second one is in the server side.
function sendValue(){
var innerHtmlData= usedFrame.innerHTML;
var clearData=innerHtmlData.toLowerCase().replace(/<script[^>]*?>/g,"");
var clearData = clearData.replace(/<\/script>/g, "");
clearData = clearData.replace(/javascript/g, "");
clearData = clearData.replace(/script/g, "");
clearData = clearData.replace(/<iframe[^>]*?>/g, "");
clearData = clearData.replace(/<\/iframe>/g, "");
clearData = Encoder.htmlEncode(clearData);
__doPostBack('getHtmlData', clearData);
}
The most vital function which in another word is the hurt of this editor,is textEdit(x,y)
function which responses for all requests over text editing issues.The essential idea behind of it is using a fundamental function of the browsers which enables you to execute a command on the current document, current selection, or the given range such as bold,italic,underline,copy,delete,reformat and so on ,named execCommand
.
The code generated by the execCommand method is different in browsers. Internet Explorer uses HTML tags, Firefox, Google Chrome and Safari generate inline styles and Opera sometimes uses HTML tags, sometimes styles.
For example, if the 'bold' command is executed on a non-bold text,
•Internet Explorer and Opera generate a strong element around it,
•Firefox, Google Chrome and Safari generate a span element around it and set the fontWeight style property of the span element to 'bold'. If an element exists around the non-bold text, then Firefox, Google Chrome and Safari set the fontWeight style property of this element to 'bold'.
If the 'bold' command is executed on a bold text, browsers remove the specified style property and/or the element including the text.
This caused that i encountered with problems in recognizing bold or underline and italic text to demonstrate it in the shape of bold,italic and underline button.Anyway i resolved it for firefox and IE but it has some problems in other browsers such Chrome,opera and firefox. getActiveButtons()
method do that as follows :
function getActiveButtons(){
isColorPickerInAction=isAdvanceColorPickerInAction=false;
var isBold=false;var isItalic=false;var isUnderline=false;
try{
if(document.all){
var xmlDocument=StringtoXML(getRangeNode(window.frames["textEditor"]));
var richTextElementsnodes=xmlDocument.documentElement.getElementsByTagName("nodeName");
var NodesCount=richTextElementsnodes.length;
for(var i=0;i<NodesCount;i++){
var innerNodeValue=richTextElementsnodes[i].firstChild.nodeValue;
switch(innerNodeValue){
case "STRONG":
isBold=true;
break;
case "EM":
isItalic=true;
break;
case "U":
isUnderline=true;
break;
}
}
}
else{
var retrivedData=getRangeNode(textEditorElement.contentWindow);
for(var i=0;i<retrivedData.length;i++){
switch(removeSpaces(retrivedData[i])){
case "font-style:italic":
isItalic=true;
break;
case "text-decoration:underline":
isUnderline=true;
break;
case "font-weight:bold":
isBold=true;
break;
}
}
}
}
catch(e){
}
setActivationStatus(isBold,isItalic,isUnderline);
}
The StringtoXML()
Method converts input string (which is generated from getRangeNode()
method) to XML :
function StringtoXML(text){
var doc;
if (window.ActiveXObject){
doc=new ActiveXObject('Microsoft.XMLDOM');
doc.async='false';
doc.loadXML(text);
}
else{
parser=new DOMParser();
doc=parser.parseFromString(text,"text/xml");
}
return doc;
} </span>
getRangeNode()
method gets the selected text parent node and its styles for recognizing the selected text style :
function getRangeNode(win){
var retrivedString="";
checkCursor(usedFrame);
if (window.getSelection){
node = win.getSelection().anchorNode;
var nodeStyleAttribute=node.parentNode.getAttributeNode("style").nodeValue.toString();
var nodeStyleAttributeChildes=nodeStyleAttribute.split(";");
retrivedString=nodeStyleAttributeChildes;
}
else if (win.document.selection){
var range = win.document.selection.createRange();
if (range){
node = range.parentElement();
nodesList="";
retrivedString= "<nodesList>"+getParentNodesList(node)+
"<nodeName>"+node.nodeName+
"</nodeName>"+"</nodesList>";
}
}
return retrivedString;
}
The syntax is : object.execCommand(cmdID, showUI, value). The execCommand function abilities and compatibilities are different in various browsers,hence in this component there are some abilities like "document saving" or "background color" which are not executed in other browsers except IE. As you can see below there are lots of incompatibilities between browsers which makes our job more difficult and it seems it goes to be more improved than before.
backcolor | yes | yes | yes | yes |
Moz/Saf require the # . Moz/Op give bgcolor to block-level element selection is part of, IE/Saf to selection itself.
To get the IE/Saf effect in Moz, use hilitecolor .
|
bold | yes | yes | yes | yes |
Notes |
contentReadOnly | no | yes | ? | no |
IE gives error |
copy | yes | protected | yes | protected |
Ctrl+C always works |
createbookmark | ? | ? | ? | ? |
Notes |
createlink | yes | yes | yes | yes |
Notes |
cut | yes | protected | yes | protected |
Ctrl+X always works |
decreasefontsize | no | yes | ? | incorrect |
Op allows only 1 decrease; the 2nd reverts text to original font size. |
delete | yes | yes | yes | yes |
Notes |
fontname | yes | yes | yes | yes |
Notes |
fontsize | no | horrid | yes | horrid |
Moz/Op generate a (gulp!) <font> tag with size equal to parseInt(value). Saf creates a normal font-size CSS declaration.
|
forecolor | yes | yes | yes | yes |
Moz/Saf require # |
formatblock
(put selection in header or paragraph)
| no | yes | buggy | incomplete |
Opera changes only the first block-level element in the selection to the desired block. |
heading | no | yes | no | no |
Notes |
hilitecolor | no | yes | no | yes |
Does the same as bgcolor in IE/Saf: it gives only the selection (and not the containing block) the defined bgcolor.
|
increasefontsize | no | yes | ? | incorrect |
Op allows only 1 increase; the 2nd reverts text to original font size. |
indent | incorrect | yes | buggy | more incorrect |
Moz adds 40px of margin-left per indent. IE/Op add a (gulp!) blockquote for every indent.
When applied to an <li> , Moz/IE generate a nested <ol/ul> , but Op again inserts a <blockquote> .
|
inserthorizontalrule | yes | yes | yes | yes |
Notes |
inserthtml | no | yes | ? | no |
Notes |
insertimage | yes | yes | yes | yes |
IE allows resizing of image |
insertorderedlist | almost | yes | almost | yes |
If the newly created ordered list borders an existing list, IE and Safari merge the two. |
insertunorderedlist | almost | yes | yes | yes |
If the newly created unordered list borders an existing list, IE merges the two. |
insertparagraph | yes | alternative | yes | yes |
Mozilla adds a paragraph around the selected block. The other browsers delete the selected block and insert an empty paragraph which the user can fill. |
italic | yes | yes | yes | yes |
Notes |
justifycenter | yes | yes | yes | yes |
Notes |
justifyfull | yes | yes | yes | yes |
Notes |
justifyleft | yes | yes | yes | yes |
Notes |
justifyright | yes | yes | yes | yes |
Notes |
multipleselection | ? | ? | ? | ? |
Notes |
outdent | yes | yes | buggy | yes |
When applied to an <li> that's a child of a single <ol/ul> , Moz/IE move the <li> to outside the <ol/ul> , while Op doesn't react.
Unfortunately IE has an extra bug in my test page: it moves the <li> to entirely outside my test element.
|
overwrite | ? | ? | ? | ? |
Notes |
paste | yes | protected | yes | protected |
Ctrl+V always works |
print | ? | ? | ? | ? |
Notes |
redo | yes | yes | yes | yes |
Redo works in Safari, but if you do Undo/Redo too often, it crashes. Solved in 3.
If you make your own changes in the editable area, Undo/Redo continues to work in Mozilla and Safari (though it ignores your custom changes), but in IE and Opera it stops working. |
refresh | ? | ? | ? | ? |
Notes |
removeformat | ? | ? | ? | ? |
Notes |
saveas | ? | ? | ? | ? |
Notes |
selectall | ? | ? | ? | ? |
Notes |
strikethrough | yes | yes | yes | yes |
Notes |
styleWithCSS | no | yes | ? | no |
Gives a generic command that styles should be applied with CSS (true ; default) or with tags (false ). When doing execCommand("bold") the first would generate a <span style="font-weight: bold";> , the second a <b> tag. |
subscript | yes | yes | yes | yes |
IE/Moz/Op: using the same command again removes the subscript. Using subscript and superscript together gives odd effects. |
superscript | yes | yes | yes | yes |
IE/Moz/Op: using the same command again removes the superscript. Using subscript and superscript together gives odd effects. |
unbookmark | ? | ? | ? | ? |
Notes |
underline | yes | yes | yes | yes |
Notes |
undo | yes | yes | yes | yes |
Undo works in Safari, but if you do Undo/Redo too often, it crashes. Solved in 3.
If you make your own changes in the editable area, Undo/Redo continues to work in Mozilla and Safari (though it ignores your custom changes), but in IE and Opera it stops working. |
unlink | yes | yes | yes | yes |
Notes |
Another crucial issue is finding the last point of cursor which has been there. this will cause the editor knows which text element should be effected by current execCommand. This function mostly is used in IE because Firefox and others by default have this ability by theme self. This tasks is being responsible by checkCursor()
Method As you observe :
function checkCursor(where){
try{
Current=where;
if (!isToolBoxContainerDivDisabled)
{
where.focus();
if(document.all){
CarretPosition=document.selection.createRange();
if(CarretPosition.text==""){
where.focus();
}
}
}
}
catch(error){
alert(error.name + ": " + error.message);
}
}
CreateEventForGeneratedButton()
is the other vital method which allocates the specified event handlers to their own buttons which dynamically created like all buttons in dialogs such as table creator, link adder,ETC.
function CreateEventForGeneratedButton(ActionType){
try{
switch(ActionType){
case "Link":
(document.all)?SetCommandButton.attachEvent ("onclick",
SetLinkCommandEventHandler):SetCommandButton.addEventListener (
"click",SetLinkCommandEventHandler,false);
break;
case "Image":
(document.all)?SetCommandImageButton.attachEvent ("onclick",
SetImageCommandEventHandler):SetCommandImageButton.addEventListener (
"click",SetImageCommandEventHandler,false);
break;
case "insertTable":
(document.all)?SetCommandInsertTableButton.attachEvent ("onclick",
SetInsertTableCommandEventHandler):SetCommandInsertTableButton.addEventListener (
"click",SetInsertTableCommandEventHandler,false);
break;
case "insertButton":
(document.all)?SetCommandInsertButton_Button.attachEvent ("onclick",
SetInsertButtonCommandEventHandler): SetCommandInsertButton_Button.addEventListener (
"click",SetInsertButtonCommandEventHandler,false);
break;
case "insertSWF":
isToolBoxContainerDivDisabled=false;
(document.all)?SetSWFCommandButton.attachEvent ("onclick",
SetInsertSWFButtonCommandEventHandler): SetSWFCommandButton.addEventListener (
"click",SetInsertSWFButtonCommandEventHandler,false);
break;
case "uploadSWF":
(document.all)?SetSWFUploaderCommandButton.attachEvent ("onclick",
SetSWFUploaderButtonCommandEventHandler):
SetSWFUploaderCommandButton.addEventListener (
"click",SetSWFUploaderButtonCommandEventHandler,false);
break;
case "CancelingInsertButtton":
(document.all)?SetCommandCancelInsertingButton_Button.attachEvent (
"onclick",SetCancelingInsertButtonCommandEventHandler):
SetCommandCancelInsertingButton_Button.addEventListener (
"click",SetCancelingInsertButtonCommandEventHandler,false);
break;
}
}
catch(error){
alert(error.name + ": " + error.message);
}
}
The Setposition()
method set the created object position. When an object is created it needs to located on the page. This method set objects position based on mouse and page.For instance when you drag a dialog bar, it uses this method with set the related
argument to "mouse" and when an object maker is created, it uses this function with "page" as related
argument.
function SetPosition(e,elementName,Related){
try{
var ContainerElement=document.getElementById(elementName);
switch(Related){
case "Page":
if(ContainerElement!=null){
ContainerElement.style.left=(document.body.clientWidth)/2-100 +"px";
ContainerElement.style.top=(document.body.clientHeight)/2-100 +"px";
}
break;
case "Mouse":
e=e||window.event;
document.getElementById(elementName).style.display="block";
document.getElementById(elementName).style.left=e.clientX+10+"px";
document.getElementById(elementName).style.top=e.clientY +"px";
break;
}
}
catch(error){
alert(error.name + ": " + error.message);
} }
RichText.js is not based on object oriented programming but i tried to make it clear and useable.
Using this Code
For using this control you should add the control's DLL from RichTextBox\Control_dll folder in your Visual Studio Toolbox and then just drag it on your ASPX page. you will see something like below:
<%@ Page Language="C#" AutoEventWireup="true"
CodeBehind="Default.aspx.cs" Inherits="testCustom._Default" %>
<%@ Register Assembly="SigmaToolBox"
Namespace="SigmaToolBox.TextEditor" TagPrefix="sigma" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body id="body1">
<%=htmlCode %>
<form id="form1" runat="server">
<div>
<sigma:RichTextBox ID="RichTextBox1" runat="server" />
</div>
<div id="div1">
<asp:Button Style="border-style: groove"
ID="Button1" runat="server" OnClientClick="sendValue()"
Text="Get Data" OnClick="Button1_Click" />
</div>
</form>
</body>
</html>
if you want to get the created document, first you should send it with OnClientClick="sendValue()"
to the server and then get safe data with setting OnClick
event on your event raiser control like button and write your code like this:
public partial class _Default : System.Web.UI.Page
{
public string htmlCode = string.Empty;
protected void Page_Load(object sender, EventArgs e)
{
}
protected void Button1_Click(object sender, EventArgs e)
{
RichTextBox1.SetHighLevelSecurityForHtmlTags = false;
htmlCode =RichTextBox1.GetValue();
}
}
Browser Testing
The control discussed in this article was tested on Firefox 5+, IE 8 , Opera , Safari and Chrome.
IE 9 : There are no error in IE 9 but exist some lacks in its appearance :
- Two excessive scroll bar
- Dialog bars disappear at the behind of tables or texts
How to open this project with Visual studio 2010 or lower versions
Since last time i opened this project with Visual Studio 2012 for checking its reliability, maybe you have problem with opening this project by lower versions than VS2012. For fixing this problem just follow below steps:
- Open project SLN file with a text editor like notepad. In the second line you'll see this line : Microsoft Visual Studio Solution File, Format Version 12.00. Change the version to 11.00 or lower
- Save text and open the project with your VS. Enjoy it !
Conclusion
I tried that this control be user friendly and altogether I believe that working with it is so simple. Entirely it is not fault-free but it was good experience for me during all 6 months which I developed it and I hope this source code learns you some valuable points about creating an asp.net custom control and web-based HTML Editor.
There will be lots of tiny points for your learning like : addressing images from CSS files into a custom control project and Assemblyinfo and lots of other stuffs in Javascript sources. Hope to be useful.
History
November 5, 2012
- The onclick javascript error for firefox over color picker actions was fixed
- Created table appearance on Chrome and Opera was improved
October 26, 2012
- Fixing for Safari Browser
November 23 , 2009
- GetValue and SetValue methods was Added
April 10, 2009
Added features :
- SWF support
- Special characters support
- Preview support