1<?php
2/*
3 * e107 website system
4 *
5 * Copyright (C) 2008-2009 e107 Inc (e107.org)
6 * Released under the terms and conditions of the
7 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
8 *
9 * Handler - general purpose validation functions
10 *
11 * $Source: /cvs_backup/e107_0.8/e107_handlers/validator_class.php,v $
12 * $Revision$
13 * $Date$
14 * $Author$
15 *
16*/
17
18if (!defined('e107_INIT')) { exit; }
19
20// List of error numbers which may be returned from validation
21define('ERR_MISSING_VALUE','01');
22define('ERR_UNEXPECTED_VALUE','02');
23define('ERR_INVALID_CHARS', '03');
24define('ERR_TOO_SHORT', '04');
25define('ERR_TOO_LONG', '05');
26define('ERR_DUPLICATE', '06');
27define('ERR_DISALLOWED_TEXT', '07');
28define('ERR_DISALLOWED_TEXT_EXACT_MATCH', '23');
29define('ERR_FIELD_DISABLED', '08');
30define('ERR_INVALID_WORD', '09');
31define('ERR_PASSWORDS_DIFFERENT', '10');
32define('ERR_BANNED_EMAIL', '11');
33define('ERR_INVALID_EMAIL', '12');
34define('ERR_ARRAY_EXPECTED', '13');
35define('ERR_BANNED_USER', '14');
36define('ERR_FIELDS_DIFFERENT', '15');
37define('ERR_CODE_ERROR', '16');
38define('ERR_TOO_LOW', '17');
39define('ERR_TOO_HIGH', '18');
40define('ERR_GENERIC', '19');				// This requires coder-defined error text
41define('ERR_IMAGE_TOO_WIDE', '20');
42define('ERR_IMAGE_TOO_HIGH', '21');
43
44// Default error messages
45e107::includeLan(e_LANGUAGEDIR.e_LANGUAGE.'/admin/lan_validator.php');
46
47/**
48 * Validator class - used by e_model and its child classes
49 *
50 * @package e107
51 * @category e107_handlers
52 * @version 1.0
53 * @author SecretR
54 * @copyright Copyright (c) 2009, e107 Inc.
55 */
56class e_validator
57{
58	/**
59	 * @var integer Unknown error code
60	 */
61	const ERR_UNKNOWN = 0;
62
63	/**
64	 * @var integer Value not found error code
65	 */
66	const ERR_MISSING_VALUE = 101;
67
68	/**
69	 * @var integer Unexpected value type error code (bad rule)
70	 */
71	const ERR_UNEXPECTED_VALUE = 102;
72
73	/**
74	 * @var integer Invalid characters error code
75	 */
76	const ERR_INVALID_CHARS = 103;
77
78	/**
79	 * @var integer Invalid email error code
80	 */
81	const ERR_INVALID_EMAIL = 104;
82
83	/**
84	 * @var integer Field doesn't match error code
85	 */
86	const ERR_FIELDS_MATCH = 105;
87
88	/**
89	 * @var integer String too short error code
90	 */
91	const ERR_TOO_SHORT = 131;
92
93	/**
94	 * @var integer String too long error code
95	 */
96	const ERR_TOO_LONG = 132;
97
98	/**
99	 * @var integer Number too low error code
100	 */
101	const ERR_TOO_LOW = 133;
102
103	/**
104	 * @var integer Number too high error code
105	 */
106	const ERR_TOO_HIGH = 134;
107
108	/**
109	 * @var integer Array count too low error code
110	 */
111	const ERR_ARRCOUNT_LOW = 135;
112
113	/**
114	 * @var integer Array count high error code
115	 */
116	const ERR_ARRCOUNT_HIGH = 136;
117
118	/**
119	 * @var integer Type of integer expected error code
120	 */
121	const ERR_INT_EXPECTED = 151;
122
123	/**
124	 * @var integer Type of float expected error code
125	 */
126	const ERR_FLOAT_EXPECTED = 152;
127
128	/**
129	 * @var integer Instance type expected error code
130	 */
131	const ERR_INSTANCEOF_EXPECTED = 153;
132
133	/**
134	 * @var integer Array type expected error code
135	 */
136	const ERR_ARRAY_EXPECTED = 154;
137
138	/**
139	 * @var integer Generic (empty value) error code
140	 */
141	const ERR_GENERIC = 191;
142
143	/**
144	 * @var integer File not exists or not a file error code
145	 */
146	const ERR_NOT_FILE = 201;
147
148	/**
149	 * @var integer File not writable error code
150	 */
151	const ERR_WRITABLE_FILE = 202;
152
153	/**
154	 * @var integer File exceeds allowed file size error code
155	 */
156	const ERR_SIZEMIN_FILE = 203;
157
158	/**
159	 * @var integer File lower than minimal file size error code
160	 */
161	const ERR_SIZEMAX_FILE = 204;
162
163	/**
164	 * Required rules - Used by validate method
165	 *
166	 * Structure: array(type, condition, field title LAN[, condition help, validation error message]);
167	 *
168	 * @example $_required_rules['download_category_id'] = array('int', '1', 'Download Category', 'choose category')
169	 *
170	 * Validation array structure:
171	 * - type | condition =
172	 * 		- regex | regex string
173	 * 		- email | no condition required
174	 * 		- int/integer | number range in format 'min:max'
175	 * 		- float |  number range in format 'min:max'
176	 * 		- str/string | number string length range in format 'min:max'
177	 * 		- required | no condition required
178	 * 		- callback | string function name or array(class name|object, method) (static call)
179	 * 		- instanceof | string class name
180	 * 		- enum | (string) values separated by '#' e.g. 'match1#match2#match3'
181	 * 		- array | array count range in format 'min:max'
182	 * 		- compare | string field_name, value should be in format field_name => array('value1', 'value1')
183	 * 					if value1 === value1, field_name => value1 will be added to $_valid_data array
184	 * - field title LAN =
185	 * 		human readable field (data key) name
186	 * - [optional] condition help =
187	 * 		can be used for both inline field help and validation error message
188	 * - [optional] validation error message =
189	 * 		if empty condition help will be used
190	 *
191	 * @var array
192	 */
193	protected $_required_rules = array();
194
195	/**
196	 * Check data only if exist/non-empty
197	 *
198	 * @var array
199	 */
200	protected $_optional_rules = array();
201
202	/**
203	 * Contains validation error codes in format 'field=>error_code'
204	 * @var array
205	 */
206	protected $_validation_results = array();
207
208	/**
209	 * Stores validated data (only after successful {@link validateField()} call
210	 * @var array
211	 */
212	protected $_valid_data = array();
213
214	/**
215	 * Stores validate check result
216	 * @var boolean
217	 */
218	protected $_is_valid_data = true;
219
220	/**
221	 * eMessage handler namespace
222	 *
223	 * @var string
224	 */
225	protected $_message_stack = 'validator';
226
227	/**
228	 * Constructore
229	 * @param string [optional] $message_stack [optional] eMessage handler namespace
230	 * @param array [optional] $rules validation rules
231	 * @param array [optional] $optrules optional validation rules
232	 */
233	public function __construct($message_stack = '', $rules = array(), $optrules = array())
234	{
235		$this->setMessageStack($message_stack)
236			->setRules($rules)
237			->setOptionalRules($optrules);
238	}
239
240	/**
241	 * Set message stack
242	 *
243	 * @param string $mstack
244	 * @return e_validator
245	 */
246	public function setMessageStack($mstack)
247	{
248		if(!$mstack) $mstack = 'validator';
249		$this->_message_stack = $mstack;
250		return $this;
251	}
252
253	/**
254	 * @param array $rules
255	 * @return e_validator
256	 */
257	public function setRules($rules)
258	{
259		$this->_required_rules = $rules;
260		return $this;
261	}
262
263	/**
264	 * @param array $rules
265	 * @return e_validator
266	 */
267	public function setOptionalRules($rules)
268	{
269		$this->_optional_rules = $rules;
270		return $this;
271	}
272
273	/**
274	 * Add successfully validated data to the valid array
275	 *
276	 * @param string $field_name
277	 * @param mixed $value
278	 * @return e_validator
279	 */
280	protected function addValidData($field_name, $value)
281	{
282		$this->_valid_data[$field_name] = $value;
283		return $this;
284	}
285
286	/**
287	 * @return array
288	 */
289	public function getValidData()
290	{
291		return $this->_valid_data;
292	}
293
294	/**
295	 * Validate data
296	 *
297	 * @param array $data
298	 * @param boolean $availableOnly check only against available data if true
299	 * @return boolean
300	 */
301	function validate($data, $availableOnly = false)
302	{
303		$this->reset();
304
305		$rules = array_merge(array_keys($this->_required_rules), array_keys($this->_optional_rules));
306		// no rules, no check
307		if(!$rules)
308		{
309			$this->setIsValidData(true);
310			$this->_valid_data = $data;
311			return true;
312		}
313
314		$fieldList = $rules;
315		if($availableOnly) $fieldList = array_keys($data);
316
317		foreach ($rules as $field_name)
318		{
319			if(!in_array($field_name, $fieldList)) continue;
320			$value = varset($data[$field_name], null);
321			$required = $this->isRequiredField($field_name);
322			if(($required || $this->isOptionalField($field_name)) && !$this->validateField($field_name, $value, $required))
323			{
324				$this->setIsValidData(false);
325				$this->addValidateMessage($this->getFieldName($field_name, $required), $this->getErrorCode($field_name), $this->getFieldMessage($field_name, $value, $required));
326				continue;
327			}
328		}
329
330		return $this->_is_valid_data;
331	}
332
333	/**
334	 * Check if field is required
335	 *
336	 * @param string $name
337	 * @return boolean
338	 */
339	function isRequiredField($name)
340	{
341		return isset($this->_required_rules[$name]);
342	}
343
344	/**
345	 * Check if there is optional rule for this field
346	 *
347	 * @param string $name
348	 * @return boolean
349	 */
350	function isOptionalField($name)
351	{
352		return isset($this->_optional_rules[$name]);
353	}
354
355	/**
356	 * Retrieve help for the required field
357	 *
358	 * @param string $name
359	 * @return string
360	 */
361	function getFieldHelp($name, $required = true, $default = '')
362	{
363		if($required)
364		{
365			$msg = (isset($this->_required_rules[$name][3]) ? $this->_required_rules[$name][3] : $default);
366		}
367		else
368		{
369			$msg = (isset($this->_optional_rules[$name][3]) ? $this->_optional_rules[$name][3] : $default);
370		}
371
372		return defset($msg, $msg);
373	}
374
375	/**
376	 * Retrieve validation error message for the required field
377	 *
378	 * @param string $name
379	 * @param mixed $value
380	 * @return string
381	 */
382	function getFieldMessage($name, $value = '', $required = true)
383	{
384		if($required)
385		{
386			if(!isset($this->_required_rules[$name][4]))
387			{
388				$msg = $this->getFieldHelp($name, true);
389			}
390			else $msg = $this->_required_rules[$name][4];
391		}
392		else
393		{
394			if(!isset($this->_optional_rules[$name][4]))
395			{
396				$msg = $this->getFieldHelp($name, false);
397			}
398			else $msg = $this->_optional_rules[$name][4];
399		}
400
401		return ($msg ? defset($msg, $msg) : '');
402	}
403
404	/**
405	 * @param string $name
406	 * @return string
407	 */
408	function getFieldName($name, $required = true)
409	{
410		if($required)
411		{
412			$msg = (isset($this->_required_rules[$name][2]) ? $this->_required_rules[$name][2] : $name);
413		}
414		else
415		{
416			$msg = (isset($this->_optional_rules[$name][2]) ? $this->_optional_rules[$name][2] : $name);
417		}
418
419		return defset($msg, $msg);
420	}
421
422	/**
423	 * Validate single field
424	 *
425	 * @param string $name
426	 * @param string $newval
427	 * @param boolean $required
428	 * @return boolean
429	 */
430	function validateField($name, $value, $required = true)
431	{
432		if($required)
433		{
434			$type = $this->_required_rules[$name][0];
435			$cond = $this->_required_rules[$name][1];
436		}
437		else
438		{
439			if(empty($value))
440			{
441				switch($this->_optional_rules[$name][0])
442				{
443					case 'int':
444					case 'integer':
445						$value = 0;
446					break;
447
448					case 'float':
449						$value = 0.00;
450					break;
451
452					case 'array':
453						$value = array();
454					break;
455
456					default:
457						$value = '';
458					break;
459				}
460				$this->addValidData($name, $value);
461				return true;
462			}
463			$type = $this->_optional_rules[$name][0];
464			$cond = $this->_optional_rules[$name][1];
465		}
466
467		switch ($type)
468		{
469			case 'required':
470				if(empty($value))
471				{
472					$this->addValidateResult($name, self::ERR_GENERIC);
473					return false;
474				}
475				$this->addValidData($name, $value);
476				return true;
477			break;
478
479			case 'email':
480				if(!check_email($value))
481				{
482					$this->addValidateResult($name, self::ERR_INVALID_EMAIL);
483					return false;
484				}
485				$this->addValidData($name, $value);
486				return true;
487			break;
488
489			case 'regexp':
490			case 'regex':
491				if(!preg_match($cond, $value))
492				{
493					$this->addValidateResult($name, self::ERR_INVALID_CHARS);
494					return false;
495				}
496				$this->addValidData($name, $value);
497				return true;
498			break;
499
500			case 'callback':
501				if(!call_user_func($cond, $value))
502				{
503					$this->addValidateResult($name, self::ERR_INVALID_CHARS);
504					return false;
505				}
506				$this->addValidData($name, $value);
507				return true;
508			break;
509
510			case 'instanceof':
511				if(!(is_object($value) && $value instanceof $cond))
512				{
513					$this->addValidateResult($name, self::ERR_INSTANCEOF_EXPECTED);
514					return false;
515				}
516				$this->addValidData($name, $value);
517				return true;
518			break;
519
520			case 'int':
521			case 'integer':
522				if(!preg_match('/^-?[\d]+$/', $value)) // negative values support
523				{
524					$this->addValidateResult($name, self::ERR_INT_EXPECTED);
525					return false;
526				}
527				// BC! Will be removed after we replace '-' with ':' separator!
528				$tmp = $this->parseMinMax($cond);
529				if(is_numeric($tmp[0]) && (integer) $tmp[0] > (integer) $value)
530				{
531					$this->addValidateResult($name, self::ERR_TOO_LOW);
532					return false;
533				}
534				if(is_numeric(varset($tmp[1])) && (integer) $tmp[1] < (integer) $value)
535				{
536					$this->addValidateResult($name, self::ERR_TOO_HIGH);
537					return false;
538				}
539				$this->addValidData($name, intval($value));
540				return true;
541			break;
542
543			case 'str':
544			case 'string':
545			case 'text':
546			case 'varchar':
547				$tmp = $this->parseMinMax($cond);
548				$length = e107::getParser()->ustrlen($value);
549				if(is_numeric($tmp[0]) && (integer) $tmp[0] > $length)
550				{
551					$this->addValidateResult($name, self::ERR_TOO_SHORT);
552					return false;
553				}
554
555				if('varchar' == $type && !varset($tmp[1])) $tmp[1] = 255;
556
557				if(is_numeric(varset($tmp[1])) && (integer) $tmp[1] < $length)
558				{
559					$this->addValidateResult($name, self::ERR_TOO_LONG);
560					return false;
561				}
562				$this->addValidData($name, (string) $value);
563				return true;
564			break;
565
566			case 'set':
567			case 'enum':
568				$tmp = array_map('trim', explode(',', $cond));
569				if(!$value || !in_array($value, $tmp))
570				{
571					$this->addValidateResult($name, self::ERR_FIELDS_MATCH);
572					return false;
573				}
574
575				$this->addValidData($name, (string) $value);
576				return true;
577			break;
578
579			case 'float':
580				$value = e107::getParser()->toNumber($value);
581				if(!is_numeric($value))
582				{
583					$this->addValidateResult($name, self::ERR_FLOAT_EXPECTED);
584					return false;
585				}
586				$tmp = $this->parseMinMax($cond);
587				if(is_numeric($tmp[0]) && (float) $tmp[0] > (float) $value)
588				{
589					$this->addValidateResult($name, self::ERR_TOO_LOW);
590					return false;
591				}
592				if(is_numeric(varset($tmp[1])) && (float) $tmp[1] < (float) $value)
593				{
594					$this->addValidateResult($name, self::ERR_TOO_HIGH);
595					return false;
596				}
597				$this->addValidData($name, $value);
598				return true;
599			break;
600
601			case 'array':
602				if(!is_array($value))
603				{
604					$this->addValidateResult($name, self::ERR_ARRAY_EXPECTED);
605					return false;
606				}
607				$length = count($value);
608				$tmp = $this->parseMinMax($cond);
609				if(is_numeric($tmp[0]) && (integer) $tmp[0] > $length)
610				{
611					$this->addValidateResult($name, self::ERR_ARRCOUNT_LOW);
612					return false;
613				}
614				if(is_numeric(varset($tmp[1])) && (float) $tmp[1] < $length)
615				{
616					$this->addValidateResult($name, self::ERR_ARRCOUNT_HIGH);
617					return false;
618				}
619				$this->addValidData($name, $value);
620				return true;
621			break;
622
623			case 'file': // TODO - type image - validate dimensions?
624				parse_str($cond, $params);
625				$path = e107::getParser()->replaceConstants(varset($params['base']).$value);
626				if(!$value || !is_file($path))
627				{
628					$this->addValidateResult($name, self::ERR_NOT_FILE);
629					return false;
630				}
631				if(vartrue($params['writable']) && !is_writable($path))
632				{
633					$this->addValidateResult($name, self::ERR_WRITABLE_FILE);
634					return false;
635				}
636				if(vartrue($params['size']))
637				{
638					$tmp = $this->parseMinMax($cond);
639					$fs = filesize($path);
640					if(!$fs || (integer) $tmp[0] > $fs)
641					{
642						$this->addValidateResult($name, self::ERR_SIZEMIN_FILE);
643						return false;
644					}
645					elseif(is_numeric(varset($tmp[1])) && (integer) $tmp[1] < $fs)
646					{
647						$this->addValidateResult($name, self::ERR_SIZEMAX_FILE);
648						return false;
649					}
650				}
651				if(is_numeric(varset($params['maxlen'])) && (integer) $params['maxlen'] < e107::getParser()->ustrlen($value))
652				{
653					$this->addValidateResult($name, self::ERR_TOO_LONG);
654					return false;
655				}
656				$this->addValidData($name, $value);
657				return true;
658			break;
659
660			case 'compare':
661				if(!is_array($value))
662				{
663					$this->addValidateResult($name, self::ERR_UNEXPECTED_VALUE);
664					return false;
665				}
666
667				if(!($value[0] && $value[1] && $value[0] == $value[1]))
668				{
669					$this->addValidateResult($name, self::ERR_FIELDS_MATCH);
670					return false;
671				}
672
673				// check length
674				if($cond)
675				{
676					$tmp = $this->parseMinMax($cond);
677					$length = e107::getParser()->ustrlen($value[0]);
678					if(is_numeric($tmp[0]) && (integer) $tmp[0] > $length)
679					{
680						$this->addValidateResult($name, self::ERR_TOO_SHORT);
681						return false;
682					}
683					if(is_numeric(varset($tmp[1])) && (integer) $tmp[1] < $length)
684					{
685						$this->addValidateResult($name, self::ERR_TOO_LONG);
686						return false;
687					}
688				}
689				$this->addValidData($name, $value[0]);
690				return true;
691			break;
692
693			case 'compare_strict':
694				if(!is_array($value))
695				{
696					$this->addValidateResult($name, self::ERR_UNEXPECTED_VALUE);
697					return false;
698				}
699				if(!($value[0] && $value[1] && $value[0] === $value[1]))
700				{
701					$this->addValidateResult($name, self::ERR_FIELDS_MATCH);
702					return false;
703				}
704
705				// check length
706				if($cond)
707				{
708					$tmp = $this->parseMinMax($cond);
709					$length = e107::getParser()->ustrlen($value[0]);
710					if(is_numeric($tmp[0]) && (integer) $tmp[0] > $length)
711					{
712						$this->addValidateResult($name, self::ERR_TOO_SHORT);
713						return false;
714					}
715					if(is_numeric(varset($tmp[1])) && (integer) $tmp[1] < $length)
716					{
717						$this->addValidateResult($name, self::ERR_TOO_LONG);
718						return false;
719					}
720				}
721				$this->addValidData($name, $value[0]);
722				return true;
723			break;
724
725			default:
726				$this->addValidateResult($name, self::ERR_UNEXPECTED_VALUE);
727				return false;
728			break;
729		}
730	}
731
732	// moved to e_parse
733	// public function toNumber($value)
734	// {
735	// 	$larr = localeconv();
736	// 	$search = array(
737	// 		$larr['decimal_point'],
738	// 		$larr['mon_decimal_point'],
739	// 		$larr['thousands_sep'],
740	// 		$larr['mon_thousands_sep'],
741	// 		$larr['currency_symbol'],
742	// 		$larr['int_curr_symbol']
743	// 	);
744	// 	$replace = array('.', '.', '', '', '', '');
745
746	// 	return str_replace($search, $replace, $value);
747	// }
748
749	protected function parseMinMax($string)
750	{
751		return explode(':', $this->_convertConditionBC($string), 2);
752	}
753
754	private function _convertConditionBC($condition)
755	{
756		// BC! Will be removed after we replace '-' with ':' separator!
757		if(strpos($condition, ':') === false)
758		{
759			return preg_replace('/^([0-9]+)-([0-9]+)$/', '$1:$2', $condition);
760		}
761		return $condition;
762	}
763
764	/**
765	 * Add validation error to validate result stack
766	 *
767	 * @param string $field_title
768	 * @param string $err_code
769	 * @param string $err_message
770	 * @param string $custom
771	 * @return e_validator
772	 */
773	function addValidateMessage($field_title, $err_code = 0, $err_message = '', $custom = '')
774	{
775		$tp = e107::getParser();
776		$lanVars = array(
777			'x' => $field_title,
778			'y' => $err_code,
779			'z' => $this->getErrorByCode($err_code)
780		);
781
782		if($custom)
783		{
784			e107::getMessage()->addStack(sprintf($err_message, $err_code, $field_title), $this->_message_stack, (true === $custom ? E_MESSAGE_ERROR : $custom));
785			return $this;
786		}
787
788		//Additional message
789		$lan = LAN_VALIDATE_FAILMSG;
790		$dbgmsg = false;
791		if($err_message)
792		{
793			$lan = (!$field_title || strpos($err_message, '[x]') !== false ? '' : '[x] - ').$err_message; // custom, e.g. // default '<strong>&quot;%1$s&quot;</strong> field error: Custom error message. '
794			$dbgmsg = LAN_VALIDATE_FAILMSG;
795		}
796
797		//Core message
798		/*
799		$msg = sprintf(
800			$lan, // default '<strong>&quot;%1$s&quot;</strong> validation error: [#%2$d] %3$s. '
801			$field_title,
802			$err_code,
803			$this->getErrorByCode($err_code)
804		);
805		 */
806
807		//NEW - removes need for using sprintf()
808		$msg = $tp->lanVars($lan,$lanVars,true); // '[x] validation error: [y] [z].'
809
810		if($dbgmsg && defset('e107_DEBUG_LEVEL'))
811		{
812
813			e107::getMessage()->addDebug($tp->lanVars($dbgmsg,$lanVars));
814
815			/*
816			e107::getMessage()->addDebug(sprintf(
817				$dbgmsg,
818				$field_title,
819				$err_code,
820				$this->getErrorByCode($err_code)
821			));
822			 */
823		}
824
825		e107::getMessage()->addStack($msg, $this->_message_stack, E_MESSAGE_ERROR);
826
827		return $this;
828	}
829
830	/**
831	 * Get validate message array
832	 *
833	 * @param boolean $clear
834	 * @return array
835	 */
836	function getValidateMessages($clear = true)
837	{
838		return e107::getMessage()->getAll($this->_message_stack, true, $clear);
839	}
840
841	/**
842	 * Render validate messages
843	 *
844	 * @param boolean $session merge with session messages
845	 * @param boolean $clear
846	 * @return string
847	 */
848	function renderValidateMessages($session = false, $clear = true)
849	{
850		return e107::getMessage()->render($this->_message_stack, $session, $clear);
851	}
852
853	/**
854	 * @param boolean $session clear session messages as well, default true
855	 * @return e_validator
856	 */
857	function clearValidateMessages($session = true)
858	{
859		e107::getMessage()->reset(false, $this->_message_stack, $session);
860		return $this;
861	}
862
863	/**
864	 * Add validate error code for a field
865	 *
866	 * @param string $name
867	 * @param integer $code
868	 * @return e_validator
869	 */
870	function addValidateResult($name, $code)
871	{
872		$this->_validation_results[$name] = $code;
873		return $this;
874	}
875
876	/**
877	 * Get validate result array
878	 *
879	 * @param boolean $clear
880	 * @return array
881	 */
882	function getValidateResults($clear = true)
883	{
884		return $this->_validation_results;
885	}
886
887	/**
888	 * Get validate result for a field
889	 *
890	 * @param string $field
891	 * @param mixed $default
892	 * @return integer error code
893	 */
894	function getErrorCode($field, $default = 0)
895	{
896		return (isset($this->_validation_results[$field]) ? $this->_validation_results[$field] : $default);
897	}
898
899	/**
900	 * Get error string by given error code
901	 *
902	 * @param string $error_code
903	 * @return integer error code
904	 */
905	function getErrorByCode($error_code)
906	{
907		$lan = 'LAN_VALIDATE_'.$error_code;
908		return defset($lan, $lan);
909	}
910
911	/**
912	 * @return e_validator
913	 */
914	function clearValidateResults()
915	{
916		$this->_validation_results = array();
917		return $this;
918	}
919
920	/**
921	 * @return boolean
922	 */
923	function isValid()
924	{
925		return empty($this->_is_valid_data);
926	}
927
928	/**
929	 * Set validation status
930	 *
931	 * @param boolean $status
932	 * @return e_validator
933	 */
934	public function setIsValidData($status)
935	{
936		$this->_is_valid_data = (boolean) $status;
937		return $this;
938	}
939
940	/**
941	 * Reset object validate result data
942	 * @return e_validator
943	 */
944	function reset()
945	{
946		$this->setIsValidData(true);
947		$this->_valid_data = array();
948		$this->clearValidateResults()
949			->clearValidateMessages();
950
951		return $this;
952	}
953}
954
955/*
956The validator functions use an array of parameters for each variable to be validated.
957
958	The index of the parameter array is the destination field name.
959
960	Possible processing options:
961		'srcname'		- specifies the array index of the source data, where its different to the destination index
962		'dbClean'		- method for preparing the value to write to the DB (done as final step before returning). Options are:
963							- 'toDB' 	- passes final value through $tp->toDB()
964							- 'intval' 	- converts to an integer
965							- 'image'  	- checks image for size
966							- 'avatar' 	- checks an image in the avatars directory
967		'stripTags'		- strips HTML tags from the value (not an error if there are some)
968		'minLength'		- minimum length (in utf-8 characters) for the string
969		'maxLength'		- minimum length (in utf-8 characters) for the string
970		'minVal'		- lowest allowed value for numerics
971		'maxVal'		- highest allowed value for numerics
972		'longTrim'		- if set, and the string exceeds maxLength, its trimmed
973		'enablePref'	- value is processed only if the named $pref evaluates to true; otherwise any input is discarded without error
974		'dataType'		- selects special processing methods:
975							1 - array of numerics (e.g. class membership)
976
977	In general, only define an option if its to be used
978*/
979
980/*	[ Berckoff ]
981 * Added "public static " to each method as the parser generates errors (and methods are called statically everywhere)
982 */
983class validatorClass
984{
985	// Passed an array of 'source' fields and an array of definitions to validate. The definition may include the name of a validation function.
986	// Returns three arrays - one of validated results, one of failed fields and one of errors corresponding to the failed fields
987	// Normally processes only those source fields it finds (and for which it has a definition). If $addDefaults is true, sets defaults for those that have
988	//  ...one and aren't otherwise defined.
989	public static function validateFields(&$sourceFields, &$definitions, $addDefaults = FALSE)
990	{
991		global $tp, $pref;
992		$ret = array('data' => array(), 'failed' => array(), 'errors' => array());
993
994		foreach ($definitions as $dest => $defs)
995		{
996			$errNum = 0;			// Start with no error
997
998			if(!is_array($defs)) //default rule - dbClean -> toDB
999			{
1000				$defs = array('dbClean', ($defs ? $defs : 'toDB'));
1001			}
1002			$src = varset($defs['srcName'],$dest);				// Set source field name
1003			if (!isset($sourceFields[$src]))
1004			{
1005				if ($addDefaults)
1006				{
1007					if (isset($defs['default']))
1008					{
1009						$ret['data'] = $defs['default'];		// Set default value if one is specified
1010					} //...otherwise don't add the value at all
1011				}
1012				else
1013				{
1014					if (!vartrue($defs['fieldOptional']))
1015					{
1016						$ret['errors'][$dest] = ERR_MISSING_VALUE;		// No source value
1017					}
1018				}
1019			}
1020			else
1021			{	// Got a field we want, and some data to validate here
1022				$value = $sourceFields[$src];
1023				if (!$errNum && isset($defs['enablePref']))
1024				{	// Only process this field if a specified pref enables it
1025					if (!vartrue($pref[$defs['enablePref']]))
1026					{
1027						continue;			// Just loop to the next field - ignore this one.
1028					}
1029				}
1030				if (!$errNum && isset($defs['stripTags']))
1031				{
1032					$newValue = trim(strip_tags($value));
1033					if ($newValue <> $value)
1034					{
1035						$errNum = ERR_INVALID_CHARS;
1036					}
1037					$value = $newValue;
1038				}
1039				if (!$errNum && isset($defs['stripChars']))
1040				{
1041					$newValue = trim(preg_replace($defs['stripChars'], "", $value));
1042					if ($newValue <> $value)
1043					{
1044						//echo "Invalid: {$newValue} :: {$value}<br />";
1045						$errNum = ERR_INVALID_CHARS;
1046					}
1047					$value = $newValue;
1048				}
1049				if (!$errNum && isset($defs['minLength']) && ($tp->ustrlen($value) < $defs['minLength']))
1050				{
1051					if ($value == '')
1052					{
1053						if (!vartrue($defs['fieldOptional']))
1054						{
1055							$errNum = ERR_MISSING_VALUE;
1056						}
1057					}
1058					else
1059					{
1060						$errNum = ERR_TOO_SHORT;
1061					}
1062				}
1063				if (!$errNum && isset($defs['maxLength']) && $tp->ustrlen($value) > $defs['maxLength'])
1064				{
1065					if (vartrue($defs['longtrim']))
1066					{
1067						$value = substr($value,0,$defs['maxLength']);
1068					}
1069					else
1070					{
1071						$errNum = ERR_TOO_LONG;
1072					}
1073				}
1074				if (!$errNum && isset($defs['minVal']) && ($value < $defs['minVal']))
1075				{
1076					$errNum = ERR_TOO_LOW;
1077				}
1078				if (!$errNum && isset($defs['maxVal']) && ($value < $defs['maxVal']))
1079				{
1080					$errNum = ERR_TOO_HIGH;
1081				}
1082				if (!$errNum && isset($defs['fixedBlock']))
1083				{
1084					$newValue = $tp->ustrtolower($value);
1085					$temp = explode(',',$defs['fixedBlock']);
1086					foreach ($temp as $t)
1087					{
1088						if ($newValue == $tp->ustrtolower($t))
1089						{
1090							$errNum = ERR_INVALID_WORD;
1091							break;
1092						}
1093					}
1094				}
1095				if (!$errNum && isset($defs['dataType']))
1096				{
1097					switch ($defs['dataType'])
1098					{
1099						case 1 :		// Assumes we've passed an array variable to be turned into a comma-separated list of integers
1100							if (is_array($value))
1101							{
1102								$temp = array();
1103								foreach ($value as $v)
1104								{
1105									$v = trim($v);
1106									if (is_numeric($v))
1107									{
1108										$temp[] = intval($v);
1109									}
1110								}
1111								$value = implode(',', array_unique($temp));
1112							}
1113							else
1114							{
1115								$errNum = ERR_ARRAY_EXPECTED;
1116							}
1117							break;
1118						case 2 :		// Assumes we're processing a dual password field - array name for second value is one more than for first
1119							$src2 = substr($src,0,-1).(substr($src,-1,1) + 1);
1120							if (!isset($sourceFields[$src2]) || ($sourceFields[$src2] != $value))
1121							{
1122								$errNum = ERR_PASSWORDS_DIFFERENT;
1123							}
1124							break;
1125						default :
1126							$errNum = ERR_CODE_ERROR;		// Pick up bad values
1127					}
1128				}
1129				if (!$errNum)
1130				{
1131					if (isset($defs['dbClean']))
1132					{
1133						switch ($defs['dbClean'])
1134						{
1135							case 'toDB' :
1136								$value = $tp->toDB($value);
1137								break;
1138							case 'intval' :
1139								$value = intval($value);
1140								break;
1141							case 'avatar' :			// Special case of an image - may be found in the avatars directory
1142								if (preg_match('#[0-9\._]#', $value))
1143								{
1144									if (strpos('-upload-', $value) === 0)
1145									{
1146										$img = e_AVATAR_UPLOAD.str_replace('-upload-', '', $value);		// Its a user-uploaded image
1147									}
1148									elseif (strpos($value, '//') !== false)
1149									{
1150										$img = $value;			// Its a remote image
1151									}
1152									else
1153									{
1154										$img = e_AVATAR_DEFAULT.$value;		// Its a server-stored image
1155									}
1156								}
1157												// Deliberately fall through into normal image processing
1158							case 'image' :			// File is an image name.  $img may be set if we fall through from 'avatar' option - its the 'true' path to the image
1159								if (!isset($img) && isset($defs['imagePath']))
1160								{
1161									$img = $defs['imagePath'].$value;
1162								}
1163								$img = varset($img,$value);
1164								//XXX There should be no size limits - as image sizes are handled by thumb.php
1165								if ($size = getimagesize($img))
1166								{
1167									// echo "Image {$img} size: {$size[0]} x {$size[1]}<br />";
1168									if (isset($defs['maxWidth']) && $size[0] > $defs['maxWidth'])
1169									{		// Image too wide
1170									//	$errNum = ERR_IMAGE_TOO_WIDE;
1171									}
1172									if (isset($defs['maxHeight']) && $size[1] > $defs['maxHeight'])
1173									{		// Image too high
1174									//	$errNum = ERR_IMAGE_TOO_HIGH;
1175									}
1176								}
1177								else
1178								{
1179									// echo "Image {$img} not found or cannot size - original value {$value}<br />";
1180								}
1181								unset($img);
1182								break;
1183							default :
1184								echo "Invalid dbClean method: {$defs['dbClean']}<br />";	// Debug message
1185						}
1186					}
1187					$ret['data'][$dest] = $value;			// Success!!
1188				}
1189			}
1190			if ($errNum)
1191			{  // error to report
1192				$ret['errors'][$dest] = $errNum;
1193				if ($defs['dataType'] == 2)
1194				{
1195					$ret['failed'][$dest] = str_repeat('*',strlen($sourceFields[$src]));		// Save value with error - obfuscated
1196				}
1197				else
1198				{
1199					$ret['failed'][$dest] = $sourceFields[$src];		// Save value with error
1200				}
1201			}
1202		}
1203		return $ret;
1204	}
1205
1206
1207/*
1208	// Validate data against a DB table
1209	//  Inspects the passed array of user data (not necessarily containing all possible fields) and validates against the DB where appropriate.
1210	//  Just skips over fields for which we don't have a validation routine without an error
1211	//	The target array is as returned from validateFields(), so has 'data', 'failed' and 'errors' first-level sub-arrays
1212	//  All the 'vetting methods' begin 'vet', and don't overlap with validateFields(), so the same definition array may be used for both
1213	//	Similarly, error numbers don't overlap with validateFields()
1214	//	Typically checks for unacceptable duplicates, banned users etc
1215	//	Any errors are reflected by updating the passed array.
1216	//	Returns TRUE if all data validates, FALSE if any field fails to validate. Checks all fields which are present, regardless
1217	//  For some things we need to know the user_id of the data being validated, so may return an error if that isn't specified
1218
1219	Parameters:
1220		'vetMethod' - see list below. To use more than one method, specify comma-separated
1221		'vetParam' - possible parameter for some vet methods
1222
1223	Valid 'vetMethod' values (use comma separated list for multiple vetting):
1224		0 - Null method
1225		1 - Check for duplicates - field name in table must be the same as array index unless 'dbFieldName' specifies otherwise
1226		2 - Check against the comma-separated wordlist in the $pref named in vetParam['signup_disallow_text']
1227		3 - Check email address against remote server, only if option enabled
1228
1229*/
1230	public static function dbValidateArray(&$targetData, &$definitions, $targetTable, $userID = 0)
1231	{
1232		global $pref;
1233		$u_sql = new db;
1234		$allOK = TRUE;
1235		$userID = intval($userID);			// Precautionary
1236		$errMsg = '';
1237		if (!$targetTable) return FALSE;
1238		foreach ($targetData['data'] as $f => $v)
1239		{
1240			$errMsg = '';
1241			if (isset($definitions[$f]))
1242			{
1243				$options = $definitions[$f];			// Validation options to use
1244				if (!vartrue($options['fieldOptional']) || ($v != ''))
1245				{
1246					$toDo = explode(',',$options['vetMethod']);
1247					foreach ($toDo as $vm)
1248					{
1249						switch ($vm)
1250						{
1251							case 0 :		// Shouldn't get this - just do nothing if we do
1252								break;
1253							case 1 :		// Check for duplicates.
1254								if ($v == '')
1255								{
1256									$errMsg = ERR_MISSING_VALUE;
1257									break;
1258								}
1259								$field = varset($options['dbFieldName'],$f);
1260								if ($temp = $u_sql->count($targetTable, "(*)", "WHERE `{$f}`='".filter_var($v, FILTER_SANITIZE_STRING)."' AND `user_id` != ".$userID))
1261								{
1262									$errMsg = ERR_DUPLICATE;
1263								}
1264//								echo "Duplicate check: {$f} = {$v} Result: {$temp}<br />";
1265								break;
1266							case 2 :		// Check against $pref
1267								if (isset($options['vetParam']) && isset($pref[$options['vetParam']]))
1268								{
1269									$tmp = explode(",", $pref[$options['vetParam']]);
1270									foreach($tmp as $disallow)
1271									{
1272										if ('!' == substr(trim($disallow), -1) && $v == str_replace('!', '', $disallow))
1273										{	// Exact match search (noticed with exclamation mark in the end of the word)
1274											$errMsg = ERR_DISALLOWED_TEXT_EXACT_MATCH;
1275										}
1276										elseif(stristr($v, trim($disallow)))
1277										{	// Wild card search
1278											$errMsg = ERR_DISALLOWED_TEXT;
1279										}
1280									}
1281									unset($tmp);
1282								}
1283								break;
1284							case 3 :			// Check email address against remote server
1285
1286							/*	if (vartrue($pref['signup_remote_emailcheck']))
1287								{
1288									require_once(e_HANDLER."mail_validation_class.php");
1289									list($adminuser,$adminhost) = split ("@", SITEADMINEMAIL);
1290									$validator = new email_validation_class;
1291									$validator->localuser= $adminuser;
1292									$validator->localhost= $adminhost;
1293									$validator->timeout=3;
1294										//	$validator->debug=1;
1295										//	$validator->html_debug=1;
1296									if($validator->ValidateEmailBox(trim($v)) != 1)
1297									{
1298										$errMsg = ERR_INVALID_EMAIL;
1299									}
1300								}
1301							*/
1302								break;
1303							default :
1304								echo 'Invalid vetMethod: '.$options['vetMethod'].'<br />';	// Really a debug aid - should never get here
1305						}
1306						if ($errMsg) { break; }			// Just trap first error
1307					}
1308					// Add in other validation methods here
1309				}
1310			}
1311			if ($errMsg)
1312			{	// Update the error
1313				$targetData['errors'][$f] = $errMsg;
1314				$targetData['failed'][$f] = $v;
1315				unset($targetData['data'][$f]);			// Remove the valid entry
1316				$allOK = FALSE;
1317			}
1318		}
1319		return $allOK;
1320	}
1321
1322
1323	// Given a comma-separated string of required fields, and an array of data, adds an error message for each field which doesn't already have an entry.
1324	// Returns TRUE if no changes (which doesn't mean there are no errors - other routines may have found them). FALSE if new errors
1325	public static function checkMandatory($fieldList, &$target)
1326	{
1327		$fields = explode(',', $fieldList);
1328		$allOK = TRUE;
1329		foreach ($fields as $f)
1330		{
1331			if (!isset($target['data'][$f]) && !isset($target['errors'][$f]))
1332			{
1333				$allOK = FALSE;
1334				$targetData['errors'][$f] = ERR_MISSING_VALUE;
1335			}
1336		}
1337		return $allOK;
1338	}
1339
1340
1341	// Adds the _FIELD_TYPES array to the data, ready for saving in the DB.
1342	// $fieldList is the standard definition array
1343	public static function addFieldTypes($fieldList, &$target, $auxList=FALSE)
1344	{
1345		$target['_FIELD_TYPES'] = array();		// We should always want to recreate the array, even if it exists
1346		foreach ($target['data'] as $k => $v)
1347		{
1348			if (isset($fieldList[$k]) && isset($fieldList[$k]['fieldType']))
1349			{
1350				$target['_FIELD_TYPES'][$k] = $fieldList[$k]['fieldType'];
1351			}
1352			elseif (is_array($auxList) && isset($auxList[$k]))
1353			{
1354				$target['_FIELD_TYPES'][$k] = $auxList[$k];
1355			}
1356		}
1357	}
1358
1359
1360
1361	// Given two arrays, returns an array of those elements in $input which are different from the corresponding element in $refs.
1362	// If $addMissing == TRUE, includes any element in $input for which there isn't a corresponding element in $refs
1363	public static function findChanges(&$input, &$refs, $addMissing = FALSE)
1364	{
1365		$ret = array();
1366		foreach ($input as $k => $v)
1367		{
1368			if (array_key_exists($k, $refs))
1369			{
1370				if ($refs[$k] != $v) { $ret[$k] = $v; }
1371			}
1372			else
1373			{
1374				if ($addMissing) { $ret[$k] = $v; }
1375			}
1376		}
1377		return $ret;
1378	}
1379
1380
1381	// Given a vetted array of variables, generates a list of errors using the specified format string.
1382	// %n is the error number (as stored on the array)
1383	// %t is the corresponding error message, made by concatenating $constPrefix and the error number to form a constant (e.g. $constPrefix = 'USER_ERROR_')
1384	// %v calls up the entered value
1385	// %f is the field name
1386	// %x is the 'nice name' - possible if parameter list passed. Otherwise field name added
1387	// $EOL is inserted after all messages except the last.
1388	// If $EOL is an empty string, returns an array of messages.
1389	public static function makeErrorList($vars, $constPrefix, $format = '%n - %x %t: %v', $EOL = '<br />', $niceNames = NULL)
1390	{
1391		if (count($vars['errors']) == 0) return '';
1392		$eList = array();
1393		$checkNice = ($niceNames != NULL) && is_array($niceNames);
1394		foreach ($vars['errors'] as $f => $n)
1395		{
1396			$curLine = $format;
1397			$curLine = str_replace('%n', $n, $curLine);
1398			if (($n == ERR_GENERIC) && isset($vars['errortext'][$f]))
1399			{
1400				$curLine = str_replace('%t', $vars['errortext'][$f], $curLine);			// Coder-defined specific error text
1401			}
1402			else
1403			{
1404				$curLine = str_replace('%t', constant($constPrefix.$n), $curLine);		// Standard messages
1405			}
1406			if(empty($vars['failed'][$f]))
1407			{
1408				$vars['failed'][$f] = LAN_VALIDATE_191;
1409		//		print_a($vars['failed']);
1410			}
1411			$curLine = str_replace('%v', filter_var($vars['failed'][$f], FILTER_SANITIZE_SPECIAL_CHARS), $curLine);
1412			$curLine = str_replace('%f', $f, $curLine);
1413			if ($checkNice & isset($niceNames[$f]['niceName']))
1414			{
1415				$curLine = str_replace('%x', $niceNames[$f]['niceName'], $curLine);
1416			}
1417			else
1418			{
1419				$curLine = str_replace('%x', $f, $curLine);		// Just use the field name
1420			}
1421			$eList[] = $curLine;
1422		}
1423		if ($EOL == '') return $eList;
1424		return implode($EOL, $eList);
1425	}
1426}
1427
1428
1429