<?php
/**
* @version	$Id: validator.php 15522 2012-08-20 15:25:51Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2011 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

defined('FULL_PATH') or die('restricted access!');

class kValidator extends kBase {

	/**
	 * Form name, used for validation
	 *
	 * @var string
	 */
	protected $lastFormName = false;

	/**
	 * Prefix & special of last data source
	 *
	 * @var string
	 */
	protected $lastPrefixSpecial = '';

	/**
	 * Object, that holds a data to be validated
	 *
	 * @var kDBItem
	 */
	protected $dataSource = null;

	/**
	 * Holds field errors
	 *
	 * @var Array
	 * @access protected
	 */
	protected $FieldErrors = Array ();

	/**
	 * Phrases for each error pseudo
	 *
	 * @var Array
	 * @access protected
	 */
	protected $ErrorMsgs = Array (
		'required'				=>	'!la_err_required!', // Field is required
		'unique'				=>	'!la_err_unique!', // Field value must be unique
		'value_out_of_range'	=>	'!la_err_value_out_of_range!', // Field is out of range, possible values from %s to %s
		'length_out_of_range'	=>	'!la_err_length_out_of_range!', // Field is out of range
		'bad_type'				=>	'!la_err_bad_type!', // Incorrect data format, please use %s
		'invalid_format'		=>	'!la_err_invalid_format!', // Incorrect data format, please use %s
		'bad_date_format'		=>	'!la_err_bad_date_format!', // Incorrect date format, please use (%s) ex. (%s)
		'primary_lang_required'	=>	'!la_err_primary_lang_required!', // Primary Lang. value Required
	);

	/**
	 * Sets data source for validation
	 *
	 * @param kDBItem $object
	 */
	public function setDataSource(&$object)
	{
		if ( $object->getFormName() === $this->lastFormName && $object->getPrefixSpecial() === $this->lastPrefixSpecial ) {
			return ;
		}

		$this->reset();

		$this->dataSource =& $object;
		$this->lastFormName = $object->getFormName();
		$this->lastPrefixSpecial = $object->getPrefixSpecial();
	}

	/**
	 * Validate all item fields based on
	 * constraints set in each field options
	 * in config
	 *
	 * @return bool
	 * @access private
	 */
	public function Validate()
	{
		// order is critical - should be called BEFORE checking errors
		$this->dataSource->UpdateFormattersMasterFields();

		$global_res = true;
		$fields = array_keys( $this->dataSource->getFields() );

		foreach ($fields as $field) {
			// call separately, otherwise 2+ validation errors will be ignored
			$res = $this->ValidateField($field);

			$global_res = $global_res && $res;
		}

		if ( !$global_res && $this->Application->isDebugMode() ) {
			$title_info = $this->dataSource->GetTitleField();

			$item_info = Array (
				$this->dataSource->IDField . ': <strong>' . $this->dataSource->GetID() . '</strong>',
			);

			if ( $title_info && reset($title_info) ) {
				$item_info[] = key($title_info) . ': <strong>' . current($title_info) . '</strong>';
			}

			$error_msg = '	Validation failed in prefix - <strong>' . $this->dataSource->Prefix . '</strong> (' . implode('; ', $item_info) . '),
							FieldErrors follow (look at items with <strong>"pseudo"</strong> key set)<br />
							You may ignore this notice if submitted data really has a validation error';
			trigger_error(trim($error_msg), E_USER_NOTICE);

			$this->Application->Debugger->dumpVars($this->FieldErrors);
		}

		return $global_res;
	}

	/**
	 * Validates given field
	 *
	 * @param string $field
	 * @return bool
	 * @access public
	 */
	public function ValidateField($field)
	{
		$options = $this->dataSource->GetFieldOptions($field);

		$error_field = isset($options['error_field']) ? $options['error_field'] : $field;
		$res = $this->GetErrorPseudo($error_field) == '';

		$res = $res && $this->ValidateRequired($field, $options);
		$res = $res && $this->ValidateType($field, $options);
		$res = $res && $this->ValidateRange($field, $options);
		$res = $res && $this->ValidateUnique($field, $options);
		$res = $res && $this->CustomValidation($field, $options);

		return $res;
	}

	/**
	 * Check if value is set for required field
	 *
	 * @param string $field field name
	 * @param Array $params field options from config
	 * @return bool
	 * @access public
	 */
	public function ValidateRequired($field, $params)
	{
		if ( !isset($params['required']) || !$params['required'] ) {
			return true;
		}

		$value = $this->dataSource->GetDBField($field);

		if ( $this->Application->ConfigValue('TrimRequiredFields') ) {
			$value = trim($value);
		}

		if ( (string)$value == '' ) {
			$this->SetError($field, 'required');

			return false;
		}

		return true;
	}

	/**
	 * Check if value in field matches field type specified in config
	 *
	 * @param string $field field name
	 * @param Array $params field options from config
	 * @return bool
	 */
	protected function ValidateType($field, $params)
	{
		$val = $this->dataSource->GetDBField($field);
		$type_regex = "#int|integer|double|float|real|numeric|string#";

		if ( $val == '' || !isset($params['type']) || !preg_match($type_regex, $params['type']) ) {
			return true;
		}

		if ( $params['type'] == 'numeric' ) {
			trigger_error('Invalid field type <strong>' . $params['type'] . '</strong> (in ValidateType method), please use <strong>float</strong> instead', E_USER_NOTICE);
			$params['type'] = 'float';
		}

		$res = is_numeric($val);

		if ( $params['type'] == 'string' || $res ) {
			$f = 'is_' . $params['type'];
			settype($val, $params['type']);

			$res = $f($val) && ($val == $this->dataSource->GetDBField($field));
		}

		if ( !$res ) {
			$this->SetError($field, 'bad_type', null, Array ($params['type']));

			return false;
		}

		return true;
	}

	/**
	 * Check if field value is in range specified in config
	 *
	 * @param string $field field name
	 * @param Array $params field options from config
	 * @return bool
	 * @access private
	 */
	protected function ValidateRange($field, $params)
	{
		$res = true;
		$val = $this->dataSource->GetDBField($field);

		if ( isset($params['type']) && preg_match("#int|integer|double|float|real#", $params['type']) && strlen($val) > 0 ) {
			// validate number
			if ( isset($params['max_value_inc'])) {
				$res = $res && $val <= $params['max_value_inc'];
				$max_val = $params['max_value_inc'].' (inclusive)';
			}

			if ( isset($params['min_value_inc'])) {
				$res = $res && $val >= $params['min_value_inc'];
				$min_val = $params['min_value_inc'].' (inclusive)';
			}

			if ( isset($params['max_value_exc'])) {
				$res = $res && $val < $params['max_value_exc'];
				$max_val = $params['max_value_exc'].' (exclusive)';
			}

			if ( isset($params['min_value_exc'])) {
				$res = $res && $val > $params['min_value_exc'];
				$min_val = $params['min_value_exc'].' (exclusive)';
			}
		}

		if ( !$res ) {
			if ( !isset($min_val) ) $min_val = '-&infin;';
			if ( !isset($max_val) ) $max_val = '&infin;';

			$this->SetError($field, 'value_out_of_range', null, Array ($min_val, $max_val));

			return false;
		}

		// validate string
		if ( isset($params['max_len']) ) {
			$res = $res && mb_strlen($val) <= $params['max_len'];
		}

		if ( isset($params['min_len']) ) {
			$res = $res && mb_strlen($val) >= $params['min_len'];
		}

		if ( !$res ) {
			$error_params = Array ((int)getArrayValue($params, 'min_len'), (int)getArrayValue($params, 'max_len'), mb_strlen($val));
			$this->SetError($field, 'length_out_of_range', null, $error_params);

			return false;
		}

		return true;
	}

	/**
	 * Validates that current record has unique field combination among other table records
	 *
	 * @param string $field field name
	 * @param Array $params field options from config
	 * @return bool
	 * @access private
	 */
	protected function ValidateUnique($field, $params)
	{
		$unique_fields = getArrayValue($params, 'unique');

		if ( $unique_fields === false ) {
			return true;
		}

		$where = Array ();
		array_push($unique_fields, $field);

		foreach ($unique_fields as $unique_field) {
			// if field is not empty or if it is required - we add where condition
			$field_value = $this->dataSource->GetDBField($unique_field);

			if ( (string)$field_value != '' || $this->dataSource->isRequired($unique_field) ) {
				$where[] = '`' . $unique_field . '` = ' . $this->Conn->qstr($field_value);
			}
			else {
				// not good if we check by less fields than indicated
				return true;
			}
		}

		// This can ONLY happen if all unique fields are empty and not required.
		// In such case we return true, because if unique field is not required there may be numerous empty values
		// if (!$where) return true;

		$sql = 'SELECT COUNT(*)
				FROM %s
				WHERE (' . implode(') AND (', $where) . ') AND (' . $this->dataSource->IDField . ' <> ' . (int)$this->dataSource->GetID() . ')';

		$res_temp = $this->Conn->GetOne( str_replace('%s', $this->dataSource->TableName, $sql) );

		$current_table_only = getArrayValue($params, 'current_table_only'); // check unique record only in current table
		$res_live = $current_table_only ? 0 : $this->Conn->GetOne( str_replace('%s', $this->Application->GetLiveName($this->dataSource->TableName), $sql) );

		$res = ($res_temp == 0) && ($res_live == 0);

		if ( !$res ) {
			$this->SetError($field, 'unique');

			return false;
		}

		return true;
	}

	/**
	 * Check field value by user-defined alghoritm
	 *
	 * @param string $field field name
	 * @param Array $params field options from config
	 * @return bool
	 */
	protected function CustomValidation($field, $params)
	{
		return true;
	}

	/**
	 * Set's field error, if pseudo passed not found then create it with message text supplied.
	 * Don't overwrite existing pseudo translation.
	 *
	 * @param string $field
	 * @param string $pseudo
	 * @param string $error_label
	 * @param Array $error_params
	 *
	 * @return bool
	 * @access public
	 */
	public function SetError($field, $pseudo, $error_label = null, $error_params = null)
	{
		$error_field = $this->dataSource->GetFieldOption($field, 'error_field', false, $field);

		if ( $this->GetErrorPseudo($error_field) ) {
			// don't set more then one error on field
			return false;
		}

		$this->FieldErrors[$error_field]['pseudo'] = $pseudo;

		if ( isset($error_params) ) {
			if ( array_key_exists('value', $error_params) ) {
				$this->FieldErrors[$error_field]['value'] = $error_params['value'];
				unset($error_params['value']);
			}

			// additional params, that helps to determine error sources
			$this->FieldErrors[$error_field]['params'] = $error_params;
		}

		if ( isset($error_label) && !isset($this->ErrorMsgs[$pseudo]) ) {
			// label for error (only when not already set)
			$this->ErrorMsgs[$pseudo] = (substr($error_label, 0, 1) == '+') ? substr($error_label, 1) : '!'.$error_label.'!';
		}

		return true;
	}

	/**
	 * Return error message for field
	 *
	 * @param string $field
	 * @param bool $force_escape
	 * @return string
	 * @access public
	 */
	public function GetErrorMsg($field, $force_escape = null)
	{
		$error_pseudo = $this->GetErrorPseudo($field);

		if ( !$error_pseudo ) {
			return '';
		}

		// if special error msg defined in config
		$error_msgs = $this->dataSource->GetFieldOption($field, 'error_msgs', false, Array ());

		if ( isset($error_msgs[$error_pseudo]) ) {
			$msg = $error_msgs[$error_pseudo];
		}
		else {
			// fallback to defaults
			if ( !isset($this->ErrorMsgs[$error_pseudo]) ) {
				trigger_error('No user message is defined for pseudo error <strong>' . $error_pseudo . '</strong>', E_USER_WARNING);

				return $error_pseudo; //return the pseudo itself
			}

			$msg = $this->ErrorMsgs[$error_pseudo];
		}

		$msg = $this->Application->ReplaceLanguageTags($msg, $force_escape);

		if ( isset($this->FieldErrors[$field]['params']) ) {
			return vsprintf($msg, $this->FieldErrors[$field]['params']);
		}

		return $msg;
	}

	/**
	 * Returns error pseudo
	 *
	 * @param string $field
	 * @return string
	 * @access public
	 *
	 */
	public function GetErrorPseudo($field)
	{
		if ( !isset($this->FieldErrors[$field]) ) {
			return '';
		}

		return isset($this->FieldErrors[$field]['pseudo']) ? $this->FieldErrors[$field]['pseudo'] : '';
	}

	/**
	 * Removes error on field
	 *
	 * @param string $field
	 * @access public
	 */
	public function RemoveError($field)
	{
		unset( $this->FieldErrors[$field] );
	}

	/**
	 * Returns field errors
	 *
	 * @return Array
	 * @access public
	 */
	public function GetFieldErrors()
	{
		return $this->FieldErrors;
	}

	/**
	 * Check if item has errors
	 *
	 * @param Array $skip_fields fields to skip during error checking
	 * @return bool
	 * @access public
	 */
	public function HasErrors( $skip_fields = Array () )
	{
		$fields = array_keys( $this->dataSource->getFields() );
		$fields = array_diff($fields, $skip_fields);

		foreach ($fields as $field) {
			// if Formatter has set some error messages during values parsing
			if ( $this->GetErrorPseudo($field) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Clears all validation errors
	 *
	 * @access public
	 */
	public function reset()
	{
		$this->FieldErrors = Array();
	}
}