/*	Version: 2.0-alpha
	Created on: 10/13/2001 by Keith Townsend
	Last Updated on: 10/25/2001 by Keith Townsend
	Send bug reports to: edge9421@hotmail.com
	
	Copyright 2001
		This script may be used under license only.  Licenses and pricing information may be obtained
		by contacting Keith Townsend at edge9421@hotmail.com. Available licenses are: 
			Limited Use (1 web site)
			Unlimited Local Use (any number of sites belonging to licensee)
			Client Distribution (any number of sites belonging to licensee or clients of licensee)
		Support for this script is also available as a license option.
	
	Description:
		This set of functions provides a handy library for Regular Expression validation, as well
		as fully automated form validation that uses HTML to define the rules of the validation.
		These functions are useful for both client-side and server-side validation.
	Requirements:
		Javascript 1.3
	Usage:
		To include this file for use on the client and the server, use two script tag sets.
		Set the "src" attribute of both script tags to point to this file.  In one of the
		script tags, add runat="server".  That will make this file available to your client
		scripts and server scripts.
		
		Example: (replace "$" with "s"...they are only there because real script tags would break this script file)
			
			<$cript language="javascript" src="RegExpValidation.js"></$cript>
			<$cript language="javascript" src="RegExpValidation.js" runat="server"></$cript>
			
			NOTE:	Including script files in this manner does not work on the server-side in IIS 4.0.
					You must be using IIS 5.0 for the above syntax (second script line) to work.  If 
					you must deploy on IIS 4.0, you can copy the script from the RegExpValidation.js 
					file and put it directly in the ASP	between <$cript language="javascript" 
					runat="server"> and </$cript> tags.  Unfortunately, this defeats the reusability 
					of having one source file.  The script itself will function perfectly under both
					IIS 4.0 & IIS 5.0.  The limitation is purely IIS 4.0's failure to properly include
					the source file.  I have tried using the #include directive to include the file,
					without success.  If anyone knows a way to get IIS 4.0 to use the source file,
					please let me know.
			
		To use the validateClientForm & validateServerForm functions, you must have one or more hidden
		input fields for every visible field you wish to validate.  The hidden field name must be identical 
		to the visible field name, but prefixed by "v_".  Prefixes may also contain numeric digits between 
		the "v" and the "_" (such as "v1_" or "v99_") if more than one rule needs to be applied to the field.
		It is important to note that each distinct validation MUST have a unique name for the server side 
		validation to function correctly.  The value of the hidden field then needs to be one of the validation 
		rule definitions.  The hidden validation fields can reside anywhere in the form, but I recommend keeping
		them near their target fields or in a single group for easy maintenance.  With your form set up this 
		way, you won't have to add any specialized client-side or server-side validation script to your web page 
		for most validation cases.  If regular expression masks are not capable of handling your validation
		needs, you can create special validation functions for those cases and still use the automated form
		validation.  Specialized validation functions are prefixed with "func_", have a single parameter for
		the value being validated, and return true or false (see func_ValidateEMail for an example).
		
		For client-side validation, you can call the validateClientForm from the onSubmit event of your
		FORM tag, passing in a reference to the form object you wish to validate.  This function will loop
		through all of the form elements and display an alert box listing all errors found.  If you have
		other specialized validation requirements than cannot be handled by this function, you can call
		your own special validation function instead, then call validateClientForm from your function after
		your other validation is successful.
		
		For server-side validation, you can call the validateServerForm function from VBScript, passing a
		reference to the IIS form object, or the SmartUpload form object.  This function will do exactly the
		same validation checking that the validateClientForm does, except it returns a string containing the
		error list instead of directly displaying an alert box.  The string contains embedded <ol> & <li> tags
		to help format the output.
		
		The ValidateRegExp function is also very handy.  It is used by the validateClientForm and 
		validateServerForm functions, but if you need to do specific regular expression comparisons for 
		strings, you can call the ValidateRegExp function directly (from both JavaScript & VBScript).
		Since regular expressions aren't a feature of VBScript, this can be a very useful tool for 
		simplifying your VBScript logic.
		
		Any number of regular expression masks may be added to the Patterns object, so masks you find yourself
		using often are great candidates for being added.  Also, if you find that you want a text field to be
		optional, but still validate the contents if entered, design a regular expression mask that will accept
		an empty string OR validate a string when given (see Patterns.optionalPhone for an example).
				
		Validation Rule Syntax:
			Notes: 
				1.	within the value field, the "~" is used as a delimiter for the different parts of the rule
					(delimiter options are limited due to the fact that regular expressions are likely to use 
					nearly every character on the keyboard, especially common delimiters like ,|/\)
				2.	the first parameter of a rule is always the rule type, which is a case insensitive string
				3.	"mask", "trigger" & "ignore" represent any valid regular expression mask, Patterns object
					property, or specialized validation function that is prefixed with "func_"
				4.	"errMessage" represents the custom error message you wish to be displayed if validation fails
					(errMessage is always optional; a generic error message will be displayed if it is missing)
				5.	"operator" can be ==, !=, <, or >
				6.	"compareToField" represents the name of the text field you are comparing to
				7.	"dependentField" represents the name of the text field that is to be validated if a certain
					entry or selection has been made
					
			Other Notes:
				1.	Inputs that will be validated must us the ID attribute, which should match the name of the
					input element
		
			To compare a text field with a validation mask:
				<input type="hidden"
					name="v_TextFieldName" 
					value="text~mask~errMessage">
					
			To compare two text fields together (string comparison, useful for verifying passwords):
				<input type="hidden"
					name="v_TextFieldName"
					value="compareText~operator~compareToField~errMessage">
					
			To compare two text fields together (numeric comparison, treats strings as floats)
				<input type="hidden"
					name="v_TextFieldName"
					value="compareNumber~operator~compareToField~errMessage">
			
			To validate that a text field falls within a range of values (string comparison):
				<input type="hidden"
					name="v_TextFieldName"
					value="rangeText~min~max~errMessage">
			
			To validate that a text field falls within a range of values (numeric comparison):
				<input type="hidden"
					name="v_TextFieldName"
					value="rangeNumber~min~max~errMessage">
					
			To ensure a radio group has something selected:
				<input type="hidden"
					name="v_RadioGroupName"
					value="radio~errMessage">
					
			To validate a text field that should only be validated if a certain selection or text input has been made in another field:
				<input type="hidden"
					name="v_TriggerField"
					value="dependent~trigger~dependentField~mask~errMessage">
					
			To validate that a group of checkboxes or a select list has at least min and no more than max selections:
				<input type="hidden"
					name="v_selectListName"
					value="list~ignore~min~max~errMessage">
*/
	
//	Define the pattern dictionary object & fill it with patterns
var Patterns = new Object();
Patterns.zip = /^\d{5}(-\d{4})?$/;													// matches zip codes
Patterns.currency = /^\$\d{1,3}(,\d{3})*\.\d{2}$/;									// matches $17.23 or $14,281,545.45 or ...
Patterns.time = /^([1-9]|1[0-2]):[0-5]\d$/;											// matches 5:04 or 12:34 but not 75:83
Patterns.required = /\w/;															// required field (makes sure there is at least one "word")
Patterns.ccMonth = /^(0[1-9]|1[0-2])$/;												// makes sure credit card month is 01 through 12
Patterns.ccYear = /^[0-9][0-9]$/;													// makes sure credit card year is 00 through 99
Patterns.phone = /^(\(\d{3}\)|\d{3})( |-|.)?\d{3}( |-|.)?\d{4}$/;					// matches phone numbers
Patterns.optionalPhone = /^((\(\d{3}\)|\d{3})( |-|.)?\d{3}( |-|.)?\d{4})?$/;		// matches optional phone numbers
Patterns.optionalLongInteger = /^([0-9])*$/											// optional long integer
Patterns.SSN = /^\d{3}-?\d{2}-?\d{4}$/												// SSN with optional dashes
Patterns.TaxID = /^\d{2}-?\d{7}$/													// Company TaxID with optional dash
Patterns.ABA = /^\d{9}$/															// 9-digit bank routing number (ABA)
Patterns.numeric = /^\d+$/															// any number of numeric digits
Patterns.date = /^(0?[1-9]|1[0-2])(\/|-)(0?[1-9]|[1-2][0-9]|3[0-1])(\/|-)(19|20)?\d{2}$/	// simple date check (all days 1-31 valid without regard to month or leap year)

//	Total form validation (client-side only)
function validateClientForm(theForm){
	var elArr = theForm.elements; 																		// get the array of elements
	var ErrorList = new String();																		// variable to hold error results
	var validationPrefix = /^v\d*_/i																	// define our prefix mask
	for(var i = 0; i < elArr.length; i++)																// for each element of the form...
		with(elArr[i]){																					// with current element of the array...
			if (validationPrefix.test(name)){															// if previxed with v_ or v[number]_ we have a validation field
				var Rules = String(value).split("~");													// get validation definition array
				var TargetName = String(name).replace(validationPrefix, '');							// identify target field
				var TargetValue = String(elArr[TargetName].value);										// get target value
				var Target = eval("theForm." + TargetName);												// get target object
				switch(Rules[0].toLowerCase()){															// figure out which type of validation to perform...
					case 'text':			//syntax: text~mask[~errMessage]
						if (!ValidateRegExp(TargetValue, Rules[1]))										// test mask
							ErrorList += (Rules[2])?													// use custom error message if there is one, or buid generic error message
								("\n-" + Rules[2]):
								("\n-failure to match " + TargetValue + " from the " + TargetName + " field to '" + Rules[1] + "'");
						break;
					case 'comparetext':		//syntax: compareText~operator~compareToField[~errMessage]
						var CompareToValue = String(elArr[Rules[2]].value);								// get value of compare-to field
						if (!eval("TargetValue.toString()" + Rules[1] + "CompareToValue.toString()"))	// test string comparison
							ErrorList += (Rules[3])?													// use custom error message if there is one, or buid generic error message
								("\n-" + Rules[3]):
								("\n-failed comparison of " + TargetValue + Rules[1] + CompareToValue 
									+ " between the " + TargetName + " and " + Rules[2] + " fields");
						break;
					case 'comparenumber':	//syntax: compareNumber~operator~compareToField[~errMessage]
						var CompareToValue = String(elArr[Rules[2]].value);								// get value of compare-to field
						if (!eval(parseFloat(TargetValue) + Rules[1] + parseFloat(CompareToValue)))		// test numeric comparison
							ErrorList += (Rules[3])?													// use custom error message if there is one, or buid generic error message
								("\n-" + Rules[3]):
								("\n-failed comparison of " + parseFloat(TargetValue) + Rules[1] + parseFloat(CompareToValue)
									+ " between the " + TargetName + " and " + Rules[2] + " fields");
						break;
					case 'rangetext':		//syntax: rangeText~min~max[~errMessage]
						if (TargetValue < Rules[1] || TargetValue > Rules[2])							// test range using string comparison
							ErrorList += (Rules[3])?													// use custom error message if there is one, or buid generic error message
								("\n-" + Rules[3]):
								("\n-" + TargetValue + " in the " + TargetName + " field is not between " + Rules[1] + " and " + Rules[2] + ".");
						break;
					case 'rangenumber':		//syntax: rangeNumber~min~max[~errMessage]
						if (parseFloat(strToZero(TargetValue)) < parseFloat(Rules[1]) || parseFloat(strToZero(TargetValue)) > parseFloat(Rules[2]))	// test range using float comparison
							ErrorList += (Rules[3])?													// use custom error message if there is one, or buid generic error message
								("\n-" + Rules[3]):
								("\n-" + TargetValue + " in the " + TargetName + " field is not between " + Rules[1] + " and " + Rules[2] + ".");
						break;
					case 'radio':			//syntax: radio~[errMessage]
						for (var n = 0; n < Target.length; n++){										// for each option...
							var flgSuccess = Target[n].checked;											// flag checked status
							if (flgSuccess) break;														// break out of loop if checked
						}
						if(!flgSuccess) ErrorList += (Rules[1])?										// use custom error message if there is one, or buid generic error message
								("\n-" + Rules[1]):
								("\n-You must select one of the options for " + TargetName);
						break;
					case 'dependent':		//syntax: dependent~trigger~dependentFieldName~mask[~errMessage]
						if(TargetValue=='undefined'||Target.length){												// check if element is a group of radio buttons, check boxes or a select list
							for (var n = 0; n < Target.length; n++)													// for each option...
								if(Target[n].checked||Target[n].selected){											// see if option is checked
									var flgCheckDependent = (ValidateRegExp(Target[n].value, Rules[1]));			// see if checked option's value matches trigger mask
									if(flgCheckDependent) break;													// break out of loop if dependent needs to be validated
								}
						}else if(String(Target.checked).toString()!='undefined'){									// see if element is a single radio button or check box
							var flgCheckDependent=(Target.checked||Target.selected)?(ValidateRegExp(TargetValue, Rules[1])):false;	// if element is checked, see if value matches trigger mask
						}
						if(flgCheckDependent==false){flgCheckDependent = (ValidateRegExp(TargetValue, Rules[1]))}							// element is a text box...see if checked option's value matches trigger mask
						if (flgCheckDependent){
							var DependentValue = String(elArr[Rules[2]].value);										// get value of dependend field
							if(!ValidateRegExp(DependentValue, Rules[3]))											// test mask for dependent field
								ErrorList += (Rules[4])?															// use custom error message if there is one, or buid generic error message
									("\n-" + Rules[4]):
									("\n-the dependent field " + Rules[2] + " failed to match " + DependentValue + " to '" + Rules[3] + "'");
						}
						break;
					case 'list':			//syntax: list~ignore~min~max[~errMessage]
						var count = 0																						// initialize our checked option counter
						if(TargetValue=='undefined'||Target.length){														// check if element is a group of radio buttons, check boxes, or a select list
							for (var n = 0; n < Target.length; n++)															// for each option...
								if (!ValidateRegExp(Target[n].value, Rules[1]) || (Rules[1]=='' && Target[n].value!=''))	// see if we should ignore this option
									count+=(Target[n].checked || Target[n].selected)?1:0									// if checked or selected, increment counter
						}else																								// element is a solitary radio button or check box
							count=(Target.checked||Target.selected)?1:0														// set counter based on checked or selected status
						if (count < Rules[2] || count > Rules[3])															// check if counter is in range
							ErrorList += (Rules[4])?																		// use custom error message if there is one, or buid generic error message
								("\n-" + Rules[4]):
								("\n-incorrect number of selections have been made for " + TargetName);
						break;
				}
			}
		}
	if(ErrorList.length > 0){																			// see if we have any errors to report
		alert("The following errors were found:\n" + ErrorList);										// report all errors found
		return false;																					// return failure status
	}else
		return true;																					// return success status
}

//	Total form validation (server-side only)
function validateServerForm(objForm){
	var ErrorList = new String();																	// variable to hold error results
	var validationPrefix = /^v\d*_/i																// define our prefix mask
	for (var i = 1; i < objForm.count + 1; i++){													// for each element of the form...
		ElementName = (objForm.Item(i).name)?objForm.Item(i).name:objForm.Key(i);					// get the element name from the correct collection (ASP vs. SmartUpload, because their collections are named differently)
		if (validationPrefix.test(ElementName)){													// if previxed with "v_" or "v[number]_" we have a validation field
			var Rules = String(objForm.Item(i)).split("~");											// get validation definition array
			var TargetName = String(ElementName).replace(validationPrefix, '');						// identify target field
			var TargetValue = String(objForm.Item(TargetName));										// get target value
			switch(Rules[0].toLowerCase()){															// figure out which type of validation to perform...
				case 'text':			//syntax: text~mask[~errMessage]
					if (!ValidateRegExp(TargetValue, Rules[1]))										// test mask
						ErrorList += (Rules[2])?													// use custom error message if there is one, or buid generic error message
							("\n<li>" + Rules[2]):
							("\n<li>failure to match " + TargetValue + " from the " + TargetName + " field to '" + Rules[1] + "'");
					break;
				case 'comparetext':		//syntax: compareText~operator~compareToField[~errMessage]
					var CompareToValue = String(objForm.Item(Rules[2]));							// get value of compare-to field
					if (!eval("TargetValue.toString()" + Rules[1] + "CompareToValue.toString()"))	// test string comparison
						ErrorList += (Rules[3])?													// use custom error message if there is one, or buid generic error message
							("\n<li>" + Rules[3]):
							("\n<li>failed comparison of " + TargetValue + Rules[1] + CompareToValue 
								+ " between the " + TargetName + " and " + Rules[2] + " fields");
					break;
				case 'comparenumber':	//syntax: compareNumber~operator~compareToField[~errMessage]
					var CompareToValue = String(objForm.Item(Rules[2]));							// get value of compare-to field
					if (!eval(parseFloat(TargetValue) + Rules[1] + parseFloat(CompareToValue)))		// test numeric comparison
						ErrorList += (Rules[3])?													// use custom error message if there is one, or buid generic error message
							("\n<li>" + Rules[3]):
							("\n<li>failed comparison of " + parseFloat(TargetValue) + Rules[1] + parseFloat(CompareToValue)
								+ " between the " + TargetName + " and " + Rules[2] + " fields");
					break;
				case 'rangetext':		//syntax: rangeText~min~max[~errMessage]
					if (TargetValue < Rules[1] || TargetValue > Rules[2])							// test range using string comparison
						ErrorList += (Rules[3])?													// use custom error message if there is one, or buid generic error message
							("\n<li>" + Rules[3]):
							("\n<li>" + TargetValue + " in the " + TargetName + " field is not between " + Rules[1] + " and " + Rules[2] + ".");
					break;
				case 'rangenumber':		//syntax: rangeNumber~min~max[~errMessage]
					if (parseFloat(strToZero(TargetValue)) < parseFloat(Rules[1]) || parseFloat(strToZero(TargetValue)) > parseFloat(Rules[2]))	// test range using float comparison
						ErrorList += (Rules[3])?													// use custom error message if there is one, or buid generic error message
							("\n<li>" + Rules[3]):
							("\n<li>" + TargetValue + " in the " + TargetName + " field is not between " + Rules[1] + " and " + Rules[2] + ".");
					break;
				case 'radio':			//syntax: radio~[errMessage]
					if(String(""+TargetValue)=='undefined') ErrorList += (Rules[1])?				// use custom error message if there is one, or buid generic error message
							("\n<li>" + Rules[1]):
							("\n<li>You must select one of the options for " + TargetName);
					break;
				case 'dependent':		//syntax: dependent~trigger~dependentFieldName~mask[~errMessage]
					var elArr = TargetValue.split(", ");											// get an array of returned values
					for (var n = 0; n < elArr.length; n++)											// for each returned value...
						if (ValidateRegExp(elArr[n], Rules[1])){									// see if value matches trigger mask
							var DependentValue = String(objForm.Item(Rules[2]));					// get value of dependend field
							if(!ValidateRegExp(DependentValue, Rules[3]))							// test mask for dependent field
								ErrorList += (Rules[4])?											// use custom error message if there is one, or buid generic error message
									("\n<li>" + Rules[4]):
									("\n<li>the dependent field " + Rules[2] + " failed to match " + DependentValue + " to '" + Rules[3] + "'");
						}
					break;
				case 'list':			//syntax: list~ignore~min~max[~errMessage]
					var elArr = TargetValue.split(", ");											// get an array of returned values
					var count = 0																	// initialize our checked option counter
					for (var n = 0; n < elArr.length; n++)											// for each option...
						count+=((!ValidateRegExp(elArr[n], Rules[1]) || (Rules[1]=='' && elArr[n]!='')) && String(""+elArr[n])!='undefined')?1:0;	//increment count if this is a valid selection
					if (count < Rules[2] || count > Rules[3])										// check if count is in range
						ErrorList += (Rules[4])?													// use custom error message if there is one, or buid generic error message
							("\n<li>" + Rules[4]):
							("\n<li>incorrect number of selections have been made for " + TargetName);
					break;
			}
		}
	}
	if(ErrorList.length > 0)																		// see if we have any errors to report
		return "The following errors were found:<ol>" + ErrorList + "</ol>";						// report all errors found
	else
		return "";																					// return no errors
}

//	generic function that can be called to validate either supplied regular 
//	expressions, named dictionary expressions, or specialized functions
function ValidateRegExp(Value, rExp){
	var y = /^func_*/
	if(y.test(rExp)) return eval(rExp + "('" + Value + "');");
	var x = (Patterns[rExp])?Patterns[rExp]:new RegExp(rExp);
	return x.test(Value);
}

//	specialized e-mail validation
function func_ValidateEMail(Address){
	var bad = new RegExp("(@.*@)|(\\.\\.)|(@\\.)|(\\.@)|(^\\.)");
	var good = new RegExp("^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$");
	if (!bad.test(Address) && good.test(Address)) return true;
	return false;
}

//	specialized e-mail validation
function func_optionalValidateEMail(Address){
	if(Address=='') return true;
	return func_ValidateEMail(Address);
}

//	specialized credit card expiration validation
//	(2 digit month, 2 digit year & ensures not in past)
function func_ccExpiration(MonthYear){
	var month = MonthYear.substr(0, 2);
	var year = '20' + MonthYear.substr(2, 2);
	var today = new Date();
	if(ValidateRegExp(month, 'ccMonth'))
		if(ValidateRegExp(year.toString().substr(2, 2), 'ccYear'))
			if((month <= today.getMonth() && year == today.getYear()) || year < today.getYear()) return false;
			else return true;
	return false;
}

function func_creditCard(CardNumber){
	var Cards = new Array();
	Cards[0] = /^(51|52|53|54|55)\d{14}$/					//MasterCard
	Cards[1] = /^(4\d{12})|(4\d{15})$/						//Visa
	//Cards[2] = /^(34|37)\d{13}$/							//AmericanExpress
	//Cards[3] = /^6011\d{12}$/								//DiscoverCard
	//Cards[4] = /^(30|36|38)\d{12}$/						//DinersClub
	//Cards[5] = /^(2014|2149)\d{11}$/						//enRouteCard
	//Cards[6] = /^(3088|3096|3112|3158|3337|3528)\d{12}$/	//JCBCard
	
	for(var i = 0; i < Cards.length; i++)
		if(Cards[i].test(CardNumber)) return luhnCheck(CardNumber);
	return false
}

function luhnCheck(CardNumber){	// aka: Mod10 check
	if (!ValidateRegExp(CardNumber, '^[0-9]{13,19}$')) return false;

	var no_digit = CardNumber.length;
	var oddoeven = no_digit & 1;
	var sum = 0;

	for (var count = 0; count < no_digit; count++) {
		var digit = parseInt(CardNumber.charAt(count));
		if (!((count & 1) ^ oddoeven)) {
			digit *= 2;
			if (digit > 9) digit -= 9;
		}
		sum += digit;
	}
	return (sum % 10 == 0);
}

//	helper function to convert non-numeric strings to 0
function strToZero(anyval) { 
     anyval = ""+anyval 
     if (anyval.substring(0,1) < "0" || anyval.substring(0,1) > "9") { 
          anyval = "0" 
     } 
     return eval(anyval) 
} 

