/* 
	Name: ValidateForm.js
	Author: Keith Wagner
	Purpose: Provide form validation based on variabe types (ie int, alphanumeric, etc)
	Version: 1.0
	Revision History:
		1.0 - Initial Version
	Notes:
		- MUST be called using <script language="javascript1.3">, since javascript 1.2
		  did NOT have type conversions!
*/

var returnValue;

function ValidateField(fieldName, fieldType, fieldRequired, fieldArgs, typeMask, displayMessage)
{
	/*
		Purpose: The driver function that the user calls
		Parameters:
			fieldName: Required. Contains the value of the field that ValidateField
				is working on. (passing [this] should be all that's required)
			
			fieldType: Required. A string containing one of the following values:
				varchar = all characters are allowed
				num = numeric only (with fieldArgs optionally containing a string
					  of the upper and lower bounds)
				alpha = alphabetic only
				alphanum = alphanumeric only
				decimal = must be a decimal number (with fieldArgs optionally
				      containing a string of the upper and lower bounds, as well
				      as the number of decimal places)
				date = date only
				time = time only
				email = a syntactically valid email address
				credit = a credit card
				
			fieldRequired: Required. Either a 1 or 0, specifying if the field is required.
				If the field is required (a value of 1), and the value of the field
				is blank, the function will return false. If the field is optional
				(a value of 0), and the value is blank, the function will return true.
				
			fieldArgs: Optional. A comma-delimited string containing optional arguments
				that give extra restrictions to the fieldType chosen.
			
			typeMask: Optional. Used mainly for varchar fields. A string containing a
				mask of the proper value for the field, based on the following characters:
					# = a number
					L = a letter
					A = alphanumeric
				example: if the mask was 'LLA-####', then 'BC3-9984' would be valid,
				but '3C2-A984' would not.
									
			displayMessage: Optional. The string in here is displayed when a field fails
				to pass the validation test. If no value given, a default message is
				displayed to the user based on the fieldType, fieldName, fieldArgs, and
				typeMask.
	*/
	if (fieldRequired==1)
	{
		if (fieldName.value.length==0)
		{
			alert('The field "' + fieldName.name + '" is required.');
			return false;
		}
	}
			
	
	var fieldArgsArray = new Array();
	returnValue = true;
	
	if (fieldArgs!=null)
	{
		fieldArgsArray = fieldArgs.split(',');
	}
	
	switch(fieldType)
	{
		case 'varchar':
		{
			ValidateVarchar(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage)
			break;
		}
		case 'num':
		{
			returnValue=ValidateNum(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage);
			break;
		}
		case 'alpha':
		{
			returnValue=ValidateAlpha(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage);
			break;
		}
		case 'alphanum':
		{
			returnValue=ValidateAlphaNum(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage);
			break;
		}
		case 'decimal':
		{
			returnValue=ValidateDecimal(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage);
			break;
		}
		case 'date':
		{
			returnValue=ValidateDate(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage);
			break;
		}
		case 'time':
		{
			returnValue=ValidateTime(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage);
			break;
		}
		case 'email':
		{
			returnValue=ValidateEmail(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage);
			break;
		}
		case 'credit':
		{
			returnValue=ValidateCredit(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage);
			break;
		}
		default:
		{
			alert('ATTENTION CODER: An invalid fieldType parameter was passed to this function.');
			break;
		}
	}
	
	if (!returnValue)
	{
		if (displayMessage!='')
			alert(displayMessage);
		else
			alert('An incorrect field of type "' + fieldType + '" for the input "' + fieldName.name + '" was specified.');
			fieldName.focus() ;
	}
	return returnValue;
}

function ValidCharSet(str,charset)
{
	var result = true;
	
	for (var i=0;i<str.length;i++)
		if (charset.indexOf(str.substr(i,1))<0)
		{
			result = false;
			break;
		}
	
	return result;
}

function ValidateMask(fieldName, typeMask)
{
	/*
		Purpose: Validating that the field specified follows the mask given.
		Parameters:
			fieldName: Required. Contains the value of the field that ValidateField
				is working on. (passing [this] should be all that's required)
			
			typeMask: Required. A string containing a mask of the proper value for the
				field, based on the following characters:
					# = a number
					L = a letter
					A = alphanumeric
				example: if the mask was 'LLA-####', then 'BC3-9984' would be valid,
				but '3C2-A984' would not.
	*/
	var fieldNameValue = fieldName.value;

	if (fieldName.value.length!=typeMask.length)
		return false;
	
	for (var i=0; i<typeMask.length; i++)
	{
		//start at right-hand side and work to the left
		switch(typeMask.charAt(i))
		{
			case '*':
			{
				//numeric only
				if (!(ValidCharSet(fieldNameValue.charAt(i),'0123456789')))
					return false;
				break;
			}
			case 'L':
			{
				//letters only
				if (!(ValidCharSet(fieldNameValue.charAt(i),'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')))
					return false;
				break;
			}
			case 'A':
			{
				//Alphanumeric only
				if (!(ValidCharSet(fieldNameValue.charAt(i),'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')))
					return false;
				break;
			}
			default:
			{
				if (!(fieldNameValue.charAt(i)==typeMask.charAt(i)))
					return false;
				break;
			}
		}
	}
	
	return true;
}


function AllNumeric(testString)
{
	//A number cannot have more than one - in it, and it must be the first character in the string.
	if (testString.indexOf('-')>0)
		return false;

	return ValidCharSet(testString,'0123456789');
}

function AllLetters(testString)
{
	return ValidCharSet(testString,'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
}

function AllAlphaNumeric(testString)
{
	return ValidCharSet(testString,'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
}

function AllNumericDecimal(testString)
{
	//A decimal cannot have more than one',' in it
	if (testString.indexOf('.',testString.indexOf('.')+1)>0)
		return false;
		
	//A number cannot have more than one - in it, and it must be the first character in the string.
	if (testString.indexOf('-')>0)
		return false;

	return ValidCharSet(testString,'0123456789.-');
}

function ValidateVarchar(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage)
{
	/*	
		Validation on varchars is completely optional. Either using a mask, or using
		fieldArgs, or both. There is only one valid fieldArg for a varchar, the
		minimum length of the field:
			fieldArgs=(1...fieldName.maxlength)
	*/
	if (fieldArgsArray!=null)
		if (fieldName.value.length>=fieldArgsArray[0])
			returnValue = returnValue && true;

	if (typeMask == '')
		returnValue = returnValue && true;
	else
		returnValue = returnValue && ValidateMask(fieldName, typeMask);

	return returnValue;
}

function ValidateNum(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage)
{
	/*
		Numeric fields must be all numbers. No decimals allowed.
		Use fieldArgs to specify the lower and upper bounds of valid values:
			fieldArgs=(lower_bound, upper_bound)
	*/
	if (fieldName.value.length == 0)
		return false;
		
	if (!AllNumeric(fieldName.value))
		returnValue=false;
	else
	{
		var tempNumber = 1;
		tempNumber = (fieldName.value - 0);
		
		if (fieldArgsArray!=null)
		{
			//get field attributes, and apply them to the field value
			if (fieldArgsArray[0]!=null)
			{
				//check to see if number >= lower bound
				if (tempNumber>=fieldArgsArray[0])
					returnValue = returnValue && true;
				else
					returnValue = false;
			}
			
			if (fieldArgsArray[1]!=null)
			{
				//check to see if number >= lower bound
				if (tempNumber<=fieldArgsArray[1])
					returnValue = returnValue && true;
				else
					returnValue = false;
			}
		}
	}
	return returnValue;
}

function ValidateAlpha(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage)
{
	/*
		Alpha fields must be all letters. Optionally, use fieldArgs to specify
		if it must be all uppercase, all lowercase, and the minimum number of
		characters allowed for the field:
			fieldArgs=('A/a',1...fieldName.maxlength)
	*/
	if (!AllLetters(fieldName.value))
		returnValue=false;
	else
	{
		if (fieldArgsArray!=null)
		{
			//get field attributes, and apply them to the field value
			if (fieldArgsArray[0]!=null)
			{
				//check to see if all lowercase or all uppercase
				if (fieldArgsArray[0]=='a')
				{
					//check for all lower case
					if (fieldName.value==fieldName.value.toLowerCase())
						returnValue = returnValue && true;
					else
						returnValue = false;
				}
				else
				{
					//check for all upper case
					if (fieldName.value==fieldName.value.toUpperCase())
						returnValue = returnValue && true;
					else
						returnValue = false;
				}
			}
			
			if (fieldArgsArray[1]!=null)
			{
				//check to see if length >= lower bound
				if (fieldName.value.length>=fieldArgsArray[1])
					returnValue = returnValue && true;
				else
					returnValue = false;
			}
		}
	}
	return returnValue;
}

function ValidateAlphaNum(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage)
{
	/*
		Alpha fields must be all letters and numbers. Optionally, use fieldArgs
		to specify if it must be all uppercase, all lowercase, and the minimum
		number of characters allowed for the field:
			fieldArgs=('A/a',1...fieldName.maxlength)
	*/
	if (!AllAlphaNumeric(fieldName.value))
		returnValue=false;
	else
	{
		if (fieldArgsArray!=null)
		{
			//get field attributes, and apply them to the field value
			if (fieldArgsArray[0]!=null)
			{
				//check to see if all lowercase or all uppercase
				if (fieldArgsArray[0]=='a')
				{
					//check for all lower case
					if (fieldName.value==fieldName.value.toLowerCase())
						returnValue = returnValue && true;
					else
						returnValue = false;
				}
				else
				{
					//check for all upper case
					if (fieldName.value==fieldName.value.toUpperCase())
						returnValue = returnValue && true;
					else
						returnValue = false;
				}
			}
			
			if (fieldArgsArray[1]!=null)
			{
				//check to see if length >= lower bound
				if (fieldName.value.length>=fieldArgsArray[1])
					returnValue = returnValue && true;
				else
					returnValue = false;
			}
		}
	}
	return returnValue;
}

function ValidateDecimal(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage)
{
	/*
		Decimal fields must be all numbers, with or without a decimal in it.
		Use fieldArgs to specify lower and upper bounds of valid values,
		as well as how many places after the decimal point are valid:
			fieldArgs=(lower_bound, upper_bound, num_places)
	*/
	if (!AllNumericDecimal(fieldName.value))
		returnValue=false;
	else
	{
		var tempNumber = 1;
		tempNumber = (fieldName.value - 0);
		
		if (fieldArgsArray!=null)
		{
			//get field attributes, and apply them to the field value
			if (fieldArgsArray[0]!=null)
			{
				//check to see if number >= lower bound
				if (tempNumber>=fieldArgsArray[0])
					returnValue = returnValue && true;
				else
					returnValue = false;
			}
			
			if (fieldArgsArray[1]!=null)
			{
				//check to see if number >= lower bound
				if (tempNumber<=fieldArgsArray[1])
					returnValue = returnValue && true;
				else
					returnValue = false;
			}
		}
	}
	return returnValue;
}

function ValidateDate(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage)
{
	/*
		The date field is checked that it corresponds with the pattern 'mm-dd-yyyy' or
		'mm/dd/yyyy'. If only 2 digits are given for the year, it is turned into four
		digits, using the current year as a basis.
	*/
	var filter  = /^([0-9])+([\-])+([0-9])+([\-])+([0-9])+$/;
	var filter2 = /^([0-9])+([\/])+([0-9])+([\/])+([0-9])+$/;
	var tempArray;
	var tempDelim = '';
	
	// Regular expressions used to make sure it at least follows one of the two patterns
	if (filter.test(fieldName.value))
	{
		tempDelim = '-';
	}
	else if (filter2.test(fieldName.value))
	{
		tempDelim = '/';
	}
	else
		return false;
	/*
		Further testing must now be done, checking that the day works for the month,
		that the month is 1...12, and that the year is 2 or 4 digits.
	*/
	tempArray = fieldName.value.split(tempDelim);
	var month = (1 * tempArray[0]);
	var day = (1 * tempArray[1]);
	var year = (1 * tempArray[2]);
	
	if ((month > 12) || (month < 1))
		return false;
	if (day < 1)
		return false;
	if ((year < 1) || ((year > 99) && (year < 1000)) || (year > 9999))
		return false;
	
	// 30 days has September...
	switch(month)
	{
		case 1:
		case 3:
		case 5:
		case 7:
		case 8:
		case 10:
		case 12:
		{
			if (day > 31)
				return false;
			break;
		}
		case 2:
		{
			// need to do 'leap year' check for february.
			var leapYear = (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) ? 1 : 0;

			if ((leapYear == 1) && (day > 29))
				return false;
			if ((leapYear == 0) && (day > 28))
				return false;
			break;
		}
		case 4:
		case 6:
		case 9:
		case 11:
		{
			if (day > 30)
				return false;
			break;
		}
	}
	
	// finally, if the year is 2 digits, convert it into 4 digits.
	if (year < 100)
	{
		var time=new Date();
		var currYear=time.getYear();
		if (currYear < 2000)   		// Y2K Fix, Isaac Powell
		currYear = currYear + 1900; // http://onyx.idbsu.edu/~ipowell
		
		currYear = Math.floor(currYear / 100) * 100;
		year = year + currYear;
		
		fieldName.value = month.toString() + tempDelim + day.toString() + tempDelim + year.toString();
	}
	
	return true;
}

function ValidateTime(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage)
{
	/*
		The time field is checked that it is numeric (aside from :'s and the optional
		AM/PM). Valid fieldArgs are whether it can be 24-hour time or AM/PM, and if hours,
		minutes, and/or seconds are included:
			fieldArgs(24/AP, hasHours, hasMinutes, hasSeconds)
	*/
	
	if (fieldArgsArray==null)
	{
		alert('ATTENTION CODER: All fieldArgs for a time datatype are required.');
		return false;
	}
	else
	{
		var tempArray = fieldName.value.split(':');
		var tempHour = 0;
		var tempMinute = 0;
		var tempSecond = 0;
		var timeIndex = 0;
		var tempAP = tempArray[tempArray.length-1].split(' ');
		tempArray[tempArray.length-1] = tempAP[0];
		if (tempAP.length > 1)
			tempArray[tempArray.length] = tempAP[1];
			
		if (fieldArgsArray[1] == 1)
		{
			// the time has hours
			if (tempArray.length == timeIndex)
				return false;
			tempHour = (1 * tempArray[0]);
			timeIndex++;
		}
		if (fieldArgsArray[2] == 1)
		{
			// the time has minutes
			if (tempArray.length == timeIndex)
				return false;
			tempMinute = (1 * tempArray[timeIndex]);
			timeIndex++;
		}
		if (fieldArgsArray[3] == 1)
		{
			// the time has seconds
			if (tempArray.length == timeIndex)
				return false;
			tempSecond = (1 * tempArray[timeIndex]);
			timeIndex++;
		}
		
		if (isNaN(tempHour))
			return false;
		if (isNaN(tempMinute))
			return false;
		if (isNaN(tempSecond))
			return false;
			
		//get field attributes, and apply them to the field value
		if (fieldArgsArray[0]==null)
		{
			alert('ATTENTION CODER: All fieldArgs for a time datatype are required.');
			return false;
		}
		else
		{
			switch(fieldArgsArray[0])
			{
				case '24':
				{
					if ((tempHour < 0) || (tempHour > 23))
						return false;
					break;
				}
				case 'AP':
				{
					if ((tempHour < 1) || (tempHour > 12))
						return false;
					if (tempArray.length == timeIndex)
						return false;
					if ((tempArray[timeIndex].toUpperCase() != 'AM') && (tempArray[timeIndex].toUpperCase() != 'PM'))
						return false;
					break;
				}
				default:
					return false;
			}
		}
		if ((tempMinute < 0) || (tempMinute > 59))
			return false;
			
		if ((tempSecond < 0) || (tempSecond > 59))
			return false;
	}
		
	return true;
}

function ValidateEmail(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage)
{
	/*
		The email field is checked for the pattern '*@*.*'. No mask is applied on
		the value in this field. The field is also checked that its last string of
		characters after the last '.' is a valid top level domain (TLD).
	*/
	var filter  = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9])+$/;

	// Official TLD's:
	var officialTLD = new Array('AERO','BIZ','COOP','COM','EDU','GOV','INFO','MIL','MUSEUM','NAME','NET','ORG','PRO');
	
	// Unofficial TLD's:
	var unofficialTLD = new Array('CAM','MP3','AGENT','ART','ARTS','ASIA','AUCTION','AUS','BANK','CAM','CHAT','CHURCH','CLUB','CORP','DDS','DESIGN','DNS2GO','E','EMAIL','EXP','FAM','FAMILY','FAQ','FED','FILM','FIRM','FREE','FUN','G','GAME','GAMES','GAY','GER','GLOBE','GMBH','GOLF','GOV','HELP','HOLA','I','INC','INT','JPN','K12','KIDS','LAW','LEARN','LLB','LLC','LLP','LNX','LOVE','LTD','MAG','MAIL','MED','MEDIA','MP3','NETZ','NIC','NOM','NPO','PER','POL','PRICES','RADIO','RSC','SCHOOL','SCIFI','SEA','SERVICE','SEX','SHOP','SKY','SOC','SPACE','SPORT','TECH','TOUR','TRAVEL','USVI','VIDEO','WEB','WINE','WIR','WIRED','ZINE','ZOO');
	
	// Country TLD's:
	var countryTLD = new Array('AC','AD','AE','AF','AG','AI','AL','AM','AN','AO','AQ','AR','AS','AT','AU','AW','AZ','BA','BB','BD','BE','BF','BG','BH','BI','BJ','BM','BN','BO','BR','BS','BT','BV','BW','BY','BZ','CA','CC','CD','CF','CG','CH','CI','CK','CL','CM','CN','CO','CR','CU','CV','CX','CY','CZ','DE','DJ','DK','DM','DO','DZ','EC','EE','EG','EH','ER','ES','ET','FI','FJ','FK','FM','FO','FR','FX','GA','GB','GD','GE','GF','GG','GH','GI','GL','GM','GN','GP','GQ','GR','GS','GT','GU','GW','GY','HK','HM','HN','HR','HT','HU','ID','IE','IL','IM','IN','IO','IQ','IR','IS','IT','JE','JM','JO','JP','KE','KG','KH','KI','KM','KN','KP','KR','KW','KY','KZ','LA','LB','LC','LI','LK','LR','LS','LT','LU','LV','LY','MA','MC','MD','MG','MH','MK','ML','MM','MN','MO','MP','MQ','MR','MS','MT','MU','MV','MW','MX','MY','MZ','NA','NC','NE','NF','NG','NI','NL','NO','NP','NR','NU','NZ','OM','PA','PE','PF','PG','PH','PK','PL','PM','PN','PR','PS','PT','PW','PY','QA','RE','RO','RU','RW','SA','SB','SC','SD','SE','SG','SH','SI','SJ','SK','SL','SM','SN','SO','SR','ST','SV','SY','SZ','TC','TD','TF','TG','TH','TJ','TK','TM','TN','TO','TP','TR','TT','TV','TW','TZ','UA','UG','UK','UM','US','UY','UZ','VA','VC','VE','VG','VI','VN','VU','WF','WS','YE','YT','YU','ZA','ZM','ZW');
	
	if (filter.test(fieldName.value))
	{
		// test the last string to see if it matches a valid TLD.
		var tempArray = fieldName.value.split('.');
		
		var lastPart = tempArray[tempArray.length-1];
		lastPart = lastPart.toUpperCase();
		for (var i=0; i<officialTLD.length; i++)
			if (officialTLD[i] == lastPart)
				return true;

		for (var i=0; i<unofficialTLD.length; i++)
			if (unofficialTLD[i] == lastPart)
				return true;

		for (var i=0; i<countryTLD.length; i++)
			if (countryTLD[i] == lastPart)
				return true;
				
		
		return false;
	}
	else
		return false; 
		
}

function ValidateCredit(fieldName, fieldType, fieldArgsArray, typeMask, displayMessage)
{
	/*
		The credit field makes use of checksums to validate a given credit card number.
		It also makes sure the number is completely numeric, and that it is a minimum
		of 15 characters. This should be run on the credit card number field itself.
		You can pass via fieldArgs the value of the input field that contains the
		credit card company. This is required to know which card you are validating,
		and must be in the list ("mastercard","visa","amex","discover").
			fieldArgs(creditCardFieldValue)
			
		Note: The following code was adapted from
			http://www.beachnet.com/~hstiles/cardtype.html
		and
			http://www.evolt.org/article/rating/17/24700/
	*/
	if (fieldArgsArray.length==0)
		return false;

	var isValid = false;
	var ccCheckRegExp = /[^\d ]/;
	var cardNumber = fieldName.value;
	var cardType = fieldArgsArray[0];
	isValid = !ccCheckRegExp.test(cardNumber);
	
	if (isValid)
	{
		var cardNumbersOnly = cardNumber.replace(/ /g,"");
		var cardNumberLength = cardNumbersOnly.length;
		var lengthIsValid = false;
		var prefixIsValid = false;
		var prefixRegExp;
		
		switch(cardType)
		{
			case "mastercard":
				lengthIsValid = (cardNumberLength == 16);
				prefixRegExp = /^5[1-5]/;
				break;
			
			case "visa":
				lengthIsValid = (cardNumberLength == 16 || cardNumberLength == 13);
				prefixRegExp = /^4/;
				break;
			
			case "amex":
				lengthIsValid = (cardNumberLength == 15);
				prefixRegExp = /^3(4|7)/;
				break;
			
			case "discover":
				lengthIsValid = (cardNumberLength == 16);
				prefixRegExp = /^6011/;
				break;
				
			default:
				prefixRegExp = /^$/;
				alert("Card type not found");
		}
		
		prefixIsValid = prefixRegExp.test(cardNumbersOnly);
		isValid = prefixIsValid && lengthIsValid;
	}
		
	if (isValid)
	{
		var numberProduct;
		var numberProductDigitIndex;
		var checkSumTotal = 0;
		
		for (digitCounter = cardNumberLength - 1; digitCounter >= 0; digitCounter--)
		{
			checkSumTotal += parseInt(cardNumbersOnly.charAt(digitCounter));
			digitCounter--;
			numberProduct = String((cardNumbersOnly.charAt(digitCounter) * 2));
			for (var productDigitCounter = 0; productDigitCounter < numberProduct.length; productDigitCounter++)
			{
				checkSumTotal += parseInt(numberProduct.charAt(productDigitCounter));
			}
		}
		
		isValid = (checkSumTotal % 10 == 0);
	}

	return isValid;
}