<?php
/*
 * Copyright 2010-2015 by MODX, LLC.
 *
 * This file is part of xPDO.
 *
 * xPDO is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * xPDO is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * xPDO; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
 * Suite 330, Boston, MA 02111-1307 USA
 */

/**
 * The base xPDO validation classes.
 *
 * This file contains the base validation classes used by xPDO.
 *
 * @package xpdo
 * @subpackage validation
 */

/**
 * The base validation service class.
 *
 * Extend this class to customize the validation process.
 *
 * @package xpdo
 * @subpackage validation
 */
class xPDOValidator {
    public $object = null;
    public $results = array();
    public $messages = array();

    public function __construct(& $object) {
        $this->object = & $object;
        $this->object->_loadValidation(true);
    }

    /**
     * Executes validation against the object attached to this validator.
     *
     * @param array $parameters A collection of parameters.
     * @return boolean Either true or false indicating valid or invalid.
     */
    public function validate(array $parameters = array()) {
        $validated= false;
        $this->reset();
        $stopOnFail= isset($parameters['stopOnFail']) && $parameters['stopOnFail']
                ? true
                : false;
        $stopOnRuleFail= isset($parameters['stopOnRuleFail']) && $parameters['stopOnRuleFail']
                ? true
                : false;
        if (!empty($this->object->_validationRules)) {
            foreach ($this->object->_validationRules as $column => $rules) {
                $this->results[$column]= $this->object->isValidated($column);
                if (!$this->results[$column]) {
                    $columnResults= array();
                    foreach ($rules as $ruleName => $rule) {
                        $result= false;
                        if (is_array($rule['parameters'])) $rule['parameters']['column'] = $column;
                        switch ($rule['type']) {
                            case 'callable':
                                $callable= $rule['rule'];
                                if (is_callable($callable)) {
                                     $result= call_user_func_array($callable, array($this->object->_fields[$column],$rule['parameters']));
                                    if (!$result) $this->addMessage($column, $ruleName, isset($rule['parameters']['message']) ? $rule['parameters']['message'] : $ruleName . ' failed');
                                } else {
                                    $this->object->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Validation function {$callable} is not a valid callable function.");
                                }
                                break;
                            case 'preg_match':
                                $result= (boolean) preg_match($rule['rule'], $this->object->_fields[$column]);
                                if (!$result) $this->addMessage($column, $ruleName, isset($rule['parameters']['message']) ? $rule['parameters']['message'] : $ruleName . ' failed');
                                if ($this->object->xpdo->getDebug() === true)
                                    $this->object->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "preg_match validation against {$rule['rule']} resulted in " . print_r($result, 1));
                                break;
                            case 'xPDOValidationRule':
                                if ($ruleClass= $this->object->xpdo->loadClass($rule['rule'], '', false, true)) {
                                    if ($ruleObject= new $ruleClass($this, $column, $ruleName)) {
                                        $callable= array($ruleObject, 'isValid');
                                        if (is_callable($callable)) {
                                            $callableParams= array($this->object->_fields[$column], $rule['parameters']);
                                            $result= call_user_func_array($callable, $callableParams);
                                        } else {
                                            $this->object->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Validation rule class {$rule['rule']} does not have an isValid() method.");
                                        }
                                    }
                                } else {
                                    $this->object->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not load validation rule class: {$rule['rule']}");
                                }
                                break;
                            default:
                                $this->object->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Unsupported validation rule: " . print_r($rule, true));
                                break;
                        }
                        $columnResults[$ruleName]= $result;
                        if (!$result && $stopOnRuleFail) {
                            break;
                        }
                    }
                    $this->results[$column]= !in_array(false, $columnResults, true) ? true : false;
                    if (!$this->results[$column] && $stopOnFail) {
                        break;
                    }
                }
                if ($this->results[$column]) {
                    $this->object->_validated[$column]= $column;
                }
            }
            if (empty($this->results) || !in_array(false, $this->results, true)) {
                $validated = true;
                if ($this->object->xpdo->getDebug() === true)
                    $this->object->xpdo->log(xPDO::LOG_LEVEL_WARN, "Validation succeeded: " . print_r($this->results, true));
            } elseif ($this->object->xpdo->getDebug() === true) {
                $this->object->xpdo->log(xPDO::LOG_LEVEL_WARN, "Validation failed: " . print_r($this->results, true));
            }
        } else {
            if ($this->object->xpdo->getDebug() === true) $this->object->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Validation called but no rules were found.");
            $validated = true;
        }
        return $validated;
    }

    /**
     * Add a validation message to the stack.
     *
     * @param string $field The name of the field the message relates to.
     * @param string $name The name of the rule the message relates to.
     * @param mixed $message An optional message; the name of the rule is used
     * if no message is specified.
     */
    public function addMessage($field, $name, $message= null) {
        if (empty($message)) $message= $name;
        array_push($this->messages, array(
            'field' => $field,
            'name' => $name,
            'message' => $message,
        ));
    }

    /**
     * Indicates validation messages were generated by validate().
     *
     * @return boolean True if messages were generated.
     */
    public function hasMessages() {
        return (count($this->messages) > 0);
    }

    /**
     * Get the validation messages generated by validate().
     *
     * @return array An array of validation messages.
     */
    public function getMessages() {
        return $this->messages;
    }

    /**
     * Get the validation results generated by validate().
     *
     * @return array An array of boolean validation results.
     */
    public function getResults() {
        return $this->results;
    }

    /**
     * Reset the validation results and messages.
     */
    public function reset() {
        $this->results= array();
        $this->messages= array();
    }
}

/**
 * The base validation rule class.
 *
 * @package xpdo
 * @subpackage validation
 */
class xPDOValidationRule {
    public $validator = null;
    public $field = '';
    public $name = '';
    public $message = '';

    /**
     * Construct a new xPDOValidationRule instance.
     *
     * @param xPDOValidator &$validator A reference to the xPDOValidator executing this rule.
     * @param mixed $field The field being validated.
     * @param mixed $name The identifying name of the validation rule.
     * @param string $message An optional message for rule failure.
     * @return xPDOValidationRule The rule instance.
     */
    public function __construct(& $validator, $field, $name, $message= '') {
        $this->validator = & $validator;
        $this->field = $field;
        $this->name = $name;
        $this->message = (!empty($message) && $message !== '0' ? $message : $name);
    }

    /**
     * The public method for executing a validation rule.
     *
     * Extend this method to provide a reusable validation rule in your xPDOValidator instance.
     *
     * @param mixed $value The value of the field being validated.
     * @param array $options Any options expected by the rule.
     * @return boolean True if the validation rule was passed, otherwise false.
     */
    public function isValid($value, array $options = array()) {
        if (isset($options['message'])) {
            $this->setMessage($options['message']);
        }
        return true;
    }

    /**
     * Set the failure message for the rule.
     *
     * @param string $message A message intended to convey the reason for rule failure.
     */
    public function setMessage($message= '') {
        if (!empty($message) && $message !== '0') {
            $this->message= $message;
        }
    }
}

class xPDOMinLengthValidationRule extends xPDOValidationRule {
    public function isValid($value, array $options = array()) {
        $result= parent :: isValid($value, $options);
        $minLength= isset($options['value']) ? intval($options['value']) : 0;
        $result= (is_string($value) && strlen($value) >= $minLength);
        if ($result === false) {
            $this->validator->addMessage($this->field, $this->name, $this->message);
        }
        return $result;
    }
}
class xPDOMaxLengthValidationRule extends xPDOValidationRule {
    public function isValid($value, array $options = array()) {
        $result= parent :: isValid($value, $options);
        $maxLength= isset($options['value']) ? intval($options['value']) : 0;
        $result= ($maxLength > 0 && is_string($value) && strlen($value) <= $maxLength);
        if ($result === false) {
            $this->validator->addMessage($this->field, $this->name, $this->message);
        }
    }
}
class xPDOMinValueValidationRule extends xPDOValidationRule {
    public function isValid($value, array $options = array()) {
        $result= parent :: isValid($value, $options);
        $minValue= isset($options['value']) ? intval($options['value']) : 0;
        $result= ($value >= $minValue);
        if ($result === false) {
            $this->validator->addMessage($this->field, $this->name, $this->message);
        }
    }
}
class xPDOMaxValueValidationRule extends xPDOValidationRule {
    public function isValid($value, array $options = array()) {
        $result= parent :: isValid($value, $options);
        $maxValue= isset($options['value']) ? intval($options['value']) : 0;
        $result= ($value <= $maxValue);
        if ($result === false) {
            $this->validator->addMessage($this->field, $this->name, $this->message);
        }
    }
}
class xPDOObjectExistsValidationRule extends xPDOValidationRule {
    public function isValid($value, array $options = array()) {
        if (!isset($options['pk']) || !isset($options['className'])) return false;

        $result= parent :: isValid($value, $options);
        $xpdo =& $this->validator->object->xpdo;

        $obj = $xpdo->getObject($options['className'],$options['pk']);
        $result = ($obj !== null);
        if ($result === false) {
            $this->validator->addMessage($this->field, $this->name, $this->message);
        }
        return $result;
    }
}
class xPDOForeignKeyConstraint extends xPDOValidationRule {
    public function isValid($value, array $options = array()) {
        if (!isset($options['alias'])) return false;
        parent :: isValid($value, $options);
        $result= false;
        $obj=& $this->validator->object;
        $xpdo=& $obj->xpdo;

        $fkdef= $obj->getFKDefinition($options['alias']);
        if (isset ($obj->_relatedObjects[$options['alias']])) {
            if (!is_object($obj->_relatedObjects[$options['alias']])) {
                $result= false;
            }
        }

        $criteria= array ($fkdef['foreign'] => $obj->get($fkdef['local']));
        if (isset($fkdef['criteria']['foreign'])) {
            $criteria= array($fkdef['criteria']['foreign'], $criteria);
        }
        if ($object= $xpdo->getObject($fkdef['class'], $criteria)) {
           $result= ($object !== null);
        }
        if ($result === false) {
            $this->validator->addMessage($this->field, $this->name, $this->message);
        }
        return $result;
    }
}
