// live demo object constructor
function LiveDemo()
{
	//define the outline of code for the head section
	//which will be used by firefox and opera
	//to output to page areas using innerHTML
	this.headcode = '<meta http-equiv="content-type" content="text/html; charset=utf-8" />'
		+ '<title>Test Page</title>'
		+ '<base href="' + location.protocol + '//' + location.host + '/static/reference/livedemos/" />'
		+ '<style type="text/css">html, body { background:#fff; color:#000; padding:5px; }</style>'
		+ '<style type="text/css">{CSS}</style>';
	
	//define the outline of code for an entire document
	//which will be used by other browsers
	//for whom the innerHTML method won't work so we have to use document.write
	this.outline = '<!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" xml:lang="en" lang="en">'
		+ '<head>'
		+ '<meta http-equiv="content-type" content="text/html; charset=utf-8" />'
		+ '<title>Test Page</title>'
		+ '<base href="' + location.protocol + '//' + location.host + '/static/reference/livedemos/" />'
		+ '<style type="text/css">html, body { background:#fff; color:#000; padding:5px; }</style>'
		+ '<style type="text/css">{CSS}</style>'
		+ '</head>'
		+ '<body>{HTML}</body>'
		+ '</html>';
	
	//save some object references
	this.input = document.getElementById('input');
	this.css = this.input['css'];
	this.html = this.input['html'];
	this.busy = document.getElementById('busy');
	this.output = document.getElementById('output');
	
	//in IE we have to access the iframe through the frames collection
	//otherwise we don't have permission to modify its document
	if(typeof document.uniqueID != 'undefined')
	{
		this.output = window.frames['output'];
	}
	
	//get the iframe content document
	this.document = null;
	
	//for mozilla, opera, and safari
	if(typeof this.output.contentDocument!='undefined')
	{
		//get a reference to the content document
		this.document = this.output.contentDocument;
	}
	//for win/ie5.5+
	else if(typeof this.output.document!='undefined')
	{
		//get a reference to the content document
		this.document = this.output.document;
	}
		
	//don't continue if we don't have a document reference
	if(!this.document) { return; }
	
	//bind the document form handlers
	this.formhandlers(this.document, this.output);
		
	//store references to the head and body
	this.head = this.document.getElementsByTagName('head').item(0);
	this.body = this.document.getElementsByTagName('body').item(0);
	
	//bind a submit handler to the input form
	//that updates the iframe with the input code
	var self = this;
	this.input.onsubmit = function()
	{
		self.update();
		return false;
	};

	//do an automatic update now to write the default code
	this.update();
}

//bind the demo form handlers
LiveDemo.prototype.formhandlers = function(doc, frame)
{
	//reference to this
	var self = this;
	
	//bind a global submit handler to deal with form submissions 
	doc.onsubmit = function(e)
	{
		//get the form target, 
		//and return true for safety if it's not a form
		var frm = e ? e.target : frame.event.srcElement;
		if(!/form/i.test(frm.nodeName)) { return true; }
		
		//pass the form to onsubmit
		//(unless it has an onsubmit handler attribute
		// in which case we want to allow that to do its thing instead)
		if(!frm.hasAttribute('onsubmit')) { self.onsubmit(frm); }
		
		//either way return false
		return false;
	};
	
	//bind individual handlers to any forms we find as well
	//this is for IE, in which the submit event doesn't bubble
	//since IE re-calls this binding method with every page change
	//then this will get the opportunity to be applied every time
	//other browsers won't get that, but they don't need it anyway :)
	var frms = doc.getElementsByTagName('form');
	for(var i=0; i<frms.length; i++)
	{
		frms[i].onsubmit = function()
		{
			//pass the form to onsubmit
			//*** onsubmit discrimination doesn't seem to work here
			self.onsubmit(this);
			
			//either way return false
			return false;
		};
	}
	
	//plus a global click handler for the "back to form" button
	doc.onclick = function(e)
	{
		//get the target and only continue if it's the right button
		//a nice long convoluted name to avoid any possible conflict :)
		var target = e ? e.target : frame.event.srcElement;
		if(target.id && target.id == 'htmllivedemobacktoformbutton')
		{
			//if we have currentdata, 
			//pass it to the update method then nullify it
			if(typeof self.currentdata != 'undefined' 
				&& self.currentdata != null)
			{
				self.update(self.currentdata.css, self.currentdata.html);
				self.currentdata = null;
			}
		}
	};
};

//submit handler for dealing with form submissions inside demos
LiveDemo.prototype.onsubmit = function(frm)
{
	//sanitize the current textarea data and save it as a global property
	//so we can restore it with the "back to form" function
	this.currentdata = {
		'css' : this.sanitize(this.css.value, 'css'),
		'html' : this.sanitize(this.html.value, 'html')
		};
	
	//get the collection of form fields
	//then begin compiling the output html
	var fields = frm.elements,
		html = '<table cellpadding="0" summary="This table has two columns for each row, '
					+ ' showing the name of the form field, and its value">'
			 + '<caption>The form submitted the following data:</caption>'
			 + '<thead><tr>'
			 + '<th id="fieldname">Field Name</th>'
			 + '<th id="fieldvalue">Value</th>'
			 + '</tr></thead><tbody>';
	
	//iterate through the fields
	for(var i=0; i<fields.length; i++)
	{
		//don't include fields with no name
		if(!fields[i].name) { continue; }
		
		//don't include unechcked checkboxes or radios
		if((fields[i].type == 'checkbox' || fields[i].type == 'radio')
			&& !fields[i].checked) { continue; }
	
		//add this field's data			
		html += '<tr>'
			  + '<td headers="fieldname"><var>' + fields[i].name + '</var></td>'
			  + '<td headers="fieldvalue">' 
			  		+ (fields[i].value == '' 
			  				? '<em>(empty)</em>' 
			  				: ('<samp>' + fields[i].value + '<samp>')
			  				)
			  		+ '</samp></td>'
			  + '</tr>';
	}
	
	//finish the html
	html += '</tbody></table>'
		  + '<button id="htmllivedemobacktoformbutton" type="button">\u2190 Back to the form</button>';
	
	//create some CSS to style this output
	var css = 'table { width:95%; border:2px ridge #ca6; border-collapse:collapse; '
				    + ' background:#ffd; color:#333; margin:5px 0 36px 5px; '
				    + ' font:normal normal normal 0.83em/1.1 arial,sans-serif; }'
			+ 'th, td, caption { text-align:left; vertical-align:top; padding:4px 6px; }'
			+ 'caption { font-family:tahoma,sans-serif; padding:0 0 6px 6px; }'
			+ 'th { width:35%; background:#fd9; border-bottom:1px solid #ca6; font-weight:bold; '
					+ ' color:#431; text-shadow:-1px -1px 0 rgba(255,240,200,0.7); }'
			+ 'th:last-child { width:65%; }'
			+ 'td { padding:3px 6px; }'
			+ 'tr:first-child > td { padding-top:5px; }'
			+ 'tr:last-child > td { padding-bottom:6px; }'
			+ 'td > em { color:#986; font:italic normal normal 0.83em arial,sans-serif; }'
			+ 'td > var { font-style:normal; }'
			+ 'td > samp { color:#000; font:normal normal normal 1em/1.1 monospace; }'
			+ '#htmllivedemobacktoformbutton { cursor:pointer; margin-left:10px; }';
		
	//send the css and html to the update method as explicit input data
	this.update(css, html);
};

//update the output area
LiveDemo.prototype.update = function(excss, exhtml)
{
	//show the busy indicator
	this.busy.style.visibility = 'visible';
	
	//then pause before doing the rest
	//so the rendering has a chance to catch up before this heavy shit
	var self = this;
	setTimeout(function()
	{
		//if we have explicit input values, 
		if(typeof excss != 'undefined' && typeof exhtml != 'undefined')
		{
			//use those values for the CSS and HTML
			//this is so that we can pass explicit values to the output
			//independently of what was entered in the textareas
			var css = excss;
			var html = exhtml;
		}
		
		//otherwise use the textarea values
		else
		{
			//retrieve and sanitize the CSS and HTML 
			//sanitizing is to prevent scripting from running on our domain
			var css = self.sanitize(self.css.value, 'css');
			var html = self.sanitize(self.html.value, 'html');
		
			//then write them back to the input area for reference
			self.css.value = css;
			self.html.value = html;
		}
		
		//now try to output the code using the innerHTML method
		try
		{
			//compile and output the code to the iframe document
			//using innerHTML so that no history events are generated
			self.head.innerHTML = self.headcode.replace('{CSS}', css);
			self.body.innerHTML = html;
		}
		
		//if that fails use the document write method
		catch(err)
		{
			//compile the output code, from html outline
			//plus purified input css and html code
			var code = self.outline;
			code = code.replace('{CSS}', css);
			code = code.replace('{HTML}', html);
	
			//output the compiled code to the iframe document
			self.document.open();
			self.document.write(code);
			
			//re-bind the document form handlers
			self.formhandlers(self.document, self.output);

			//close the document
			self.document.close();
		}
	
		//hide the busy indicator again
		self.busy.style.visibility = 'hidden';

	//create some artificial latency so the icon always shows up
	//and for the sake of teaching the UI
	}, 100);
};

//sanitize CSS and HTML to remove scripting
LiveDemo.prototype.sanitize = function(code, type)
{
	//*** what about elements and stuff that are written partially or wholly in unicode

	//for CSS
	if(type == 'css')
	{
		//remove imports
		code = code.replace(/@import.*$/gm, '');
		
		//remove expression and behavior
		code = code.replace(/(expression|behavior).*$/gm, '');
		
		//remove url()
		code = code.replace(/url\s*\([^\)]+\)/gm, '');
		
		//remove any markup, just in case!
		code = code.replace(/<[^>]*>/gm, '');
	}
		
	//for HTML
	if(type == 'html')
	{
		//remove single-quoted event handlers
		code = code.replace(/([\s]+on[a-z]+)(\s*=\s*\')([^\']*)(\')/ig, '');
		
		//remove any unauthorized event handlers
		code = code.replace(/([\s]+on[a-z]+)(\s*=\s*\")([^\"]*)(\")/ig, 
				function(a,b,c,d,e)
				{
					//if the handler contents don't match a known notify() message pattern
					//return an empty string to remove the whole thing
					if(!/^notify\([-_a-z0-9\s\.\,\+\=\'\[\]\!\?\\]+\)(;return false;)?$/i.test(d)) { return ''; }
					
					//otherwise return the handler unmodified
					return a;
				});

		//parse element source attributes to disallow unauthorized values
		code = code.replace(/([\s]+(?:src|data|value|action|code|archive|classid|codebase|pluginspage)+)(\s*=\s*[\"\'])([^\"\']*)([\"\'])/ig, 
				function(a,b,c,d,e)
				{
					//if the attribute contains any kind of protocol, 
					//and it doesn't point to an approved domain 
					//	(reference.sitepoint, sitepoint, sitepointstatic, v4
					//   or: subdomain.sitepointstatic etc.
					//   plus: download.macromedia.com, www.macromedia.com for codebase attributes)
					//then clear its value entirely, to prevent anything 
					//except local and approved files from being included
					if(/^[a-z]+\:[\/]+/i.test(d) 
						&& !/^http\:\/\/(([a-z0-9]+\.)?(reference\.)?(sitepoint|v4)(static)?)|((download|www)?\.macromedia\.com)/i.test(d)) 
							{ return b + c + '' + e; }
					
					//if the URL was to macromedia then the attribute 
					//must be codebase or pluginspage, else clear it
					if(/^http\:\/\/((download|www)?\.macromedia\.com)/i.test(d) 
						&& !/(codebase|pluginspage)/i.test(b)) { return b + c + '' + e; }
					
					//if we get here just return the attribute unchanged
					return a;
				});

		//remove script tags (and their content)
		code = code.replace(/<script([^>]*)>.*<\/script>/gm, '');
		code = code.replace(/<[\/]?script([^>]*)([ ]?\/)?>/gm, '');
		
		//remove processing instructions
		code = code.replace(/<\?[^>]*>/gm, '');
	}
	
	//return the sanitized code
	return code;
};

//init
window.onload = function()
{
	new LiveDemo();
};
