<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Captcha
 * @subpackage Adapter
 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */

/** @see Zend_Captcha_Base */
require_once 'Zend/Captcha/Base.php';

/** @see Zend_Crypt_Math */
require_once 'Zend/Crypt/Math.php';

/**
 * Word-based captcha adapter
 *
 * Generates random word which user should recognise
 *
 * @category   Zend
 * @package    Zend_Captcha
 * @subpackage Adapter
 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id$
 */
abstract class Zend_Captcha_Word extends Zend_Captcha_Base
{
    /**#@+
     * @var array Character sets
     */
    static public $V  = ["a", "e", "i", "o", "u", "y"];
    static public $VN = ["a", "e", "i", "o", "u", "y","2","3","4","5","6","7","8","9"];
    static public $C  = ["b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z"];
    static public $CN = ["b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z","2","3","4","5","6","7","8","9"];
    /**#@-*/

    /**
     * Random session ID
     *
     * @var string
     */
    protected $_id;

    /**
     * Generated word
     *
     * @var string
     */
    protected $_word;

    /**
     * Session
     *
     * @var Zend_Session_Namespace
     */
    protected $_session;

    /**
     * Class name for sessions
     *
     * @var string
     */
    protected $_sessionClass = 'Zend_Session_Namespace';

    /**
     * Should the numbers be used or only letters
     *
     * @var boolean
     */
    protected $_useNumbers = true;

    /**
     * Should both cases be used or only lowercase
     *
     * @var boolean
     */
    // protected $_useCase = false;

    /**
     * Session lifetime for the captcha data
     *
     * @var integer
     */
    protected $_timeout = 300;

    /**
     * Should generate() keep session or create a new one?
     *
     * @var boolean
     */
    protected $_keepSession = false;

    /**#@+
     * Error codes
     */
    const MISSING_VALUE = 'missingValue';
    const MISSING_ID    = 'missingID';
    const BAD_CAPTCHA   = 'badCaptcha';
    /**#@-*/

    /**
     * Error messages
     * @var array
     */
    protected $_messageTemplates = [
        self::MISSING_VALUE => 'Empty captcha value',
        self::MISSING_ID    => 'Captcha ID field is missing',
        self::BAD_CAPTCHA   => 'Captcha value is wrong',
    ];

    /**
     * Length of the word to generate
     *
     * @var integer
     */
    protected $_wordlen = 8;

    /**
     * Retrieve session class to utilize
     *
     * @return string
     */
    public function getSessionClass()
    {
        return $this->_sessionClass;
    }

    /**
     * Set session class for persistence
     *
     * @param  string $_sessionClass
     * @return Zend_Captcha_Word
     */
    public function setSessionClass($_sessionClass)
    {
        $this->_sessionClass = $_sessionClass;
        return $this;
    }

    /**
     * Retrieve word length to use when genrating captcha
     *
     * @return integer
     */
    public function getWordlen()
    {
        return $this->_wordlen;
    }

    /**
     * Set word length of captcha
     *
     * @param integer $wordlen
     * @return Zend_Captcha_Word
     */
    public function setWordlen($wordlen)
    {
        $this->_wordlen = $wordlen;
        return $this;
    }

    /**
     * Retrieve captcha ID
     *
     * @return string
     */
    public function getId()
    {
        if (null === $this->_id) {
            $this->_setId($this->_generateRandomId());
        }
        return $this->_id;
    }

    /**
     * Set captcha identifier
     *
     * @param string $id
     * @return Zend_Captcha_Word
     */
    protected function _setId($id)
    {
        $this->_id = $id;
        return $this;
    }

    /**
     * Set timeout for session token
     *
     * @param  int $ttl
     * @return Zend_Captcha_Word
     */
    public function setTimeout($ttl)
    {
        $this->_timeout = (int) $ttl;
        return $this;
    }

    /**
     * Get session token timeout
     *
     * @return int
     */
    public function getTimeout()
    {
        return $this->_timeout;
    }

    /**
     * Sets if session should be preserved on generate()
     *
     * @param bool $keepSession Should session be kept on generate()?
     * @return Zend_Captcha_Word
     */
    public function setKeepSession($keepSession)
    {
        $this->_keepSession = $keepSession;
        return $this;
    }

    /**
     * Numbers should be included in the pattern?
     *
     * @return bool
     */
    public function getUseNumbers()
    {
        return $this->_useNumbers;
    }

    /**
     * Set if numbers should be included in the pattern
     *
     * @param bool $_useNumbers numbers should be included in the pattern?
     * @return Zend_Captcha_Word
     */
    public function setUseNumbers($_useNumbers)
    {
        $this->_useNumbers = $_useNumbers;
        return $this;
    }

    /**
     * Get session object
     *
     * @return Zend_Session_Namespace
     */
    public function getSession()
    {
        if (!isset($this->_session) || (null === $this->_session)) {
            $id = $this->getId();
            if (!class_exists($this->_sessionClass)) {
                require_once 'Zend/Loader.php';
                Zend_Loader::loadClass($this->_sessionClass);
            }
            $this->_session = new $this->_sessionClass('Zend_Form_Captcha_' . $id);
            $this->_session->setExpirationHops(1, null, true);
            $this->_session->setExpirationSeconds($this->getTimeout());
        }
        return $this->_session;
    }

    /**
     * Set session namespace object
     *
     * @param  Zend_Session_Namespace $session
     * @return Zend_Captcha_Word
     */
    public function setSession(Zend_Session_Namespace $session)
    {
        $this->_session = $session;
        if ($session) {
            $this->_keepSession = true;
        }
        return $this;
    }

    /**
     * Get captcha word
     *
     * @return string
     */
    public function getWord()
    {
        if (empty($this->_word)) {
            $session     = $this->getSession();
            $this->_word = $session->word;
        }
        return $this->_word;
    }

    /**
     * Set captcha word
     *
     * @param  string $word
     * @return Zend_Captcha_Word
     */
    protected function _setWord($word)
    {
        $session       = $this->getSession();
        $session->word = $word;
        $this->_word   = $word;
        return $this;
    }

    /**
     * Generate new random word
     *
     * @return string
     */
    protected function _generateWord()
    {
        $word       = '';
        $wordLen    = $this->getWordlen();
        $vowels     = $this->_useNumbers ? self::$VN : self::$V;
        $consonants = $this->_useNumbers ? self::$CN : self::$C;

        $totIndexCon = count($consonants) - 1;
        $totIndexVow = count($vowels) - 1;
        for ($i=0; $i < $wordLen; $i = $i + 2) {
            // generate word with mix of vowels and consonants
            $consonant = $consonants[Zend_Crypt_Math::randInteger(0, $totIndexCon, true)];
            $vowel     = $vowels[Zend_Crypt_Math::randInteger(0, $totIndexVow, true)];
            $word     .= $consonant . $vowel;
        }

        if (strlen($word) > $wordLen) {
            $word = substr($word, 0, $wordLen);
        }

        return $word;
    }

    /**
     * Generate new session ID and new word
     *
     * @return string session ID
     */
    public function generate()
    {
        if (!$this->_keepSession) {
            $this->_session = null;
        }
        $id = $this->_generateRandomId();
        $this->_setId($id);
        $word = $this->_generateWord();
        $this->_setWord($word);
        return $id;
    }

    protected function _generateRandomId()
    {
        return md5(Zend_Crypt_Math::randBytes(32));
    }

    /**
     * Validate the word
     *
     * @see    Zend_Validate_Interface::isValid()
     * @param  mixed      $value
     * @param  array|null $context
     * @return boolean
     */
    public function isValid($value, $context = null)
    {
        if (!is_array($value) && !is_array($context)) {
            $this->_error(self::MISSING_VALUE);
            return false;
        }
        if (!is_array($value) && is_array($context)) {
            $value = $context;
        }

        $name = $this->getName();

        if (isset($value[$name])) {
            $value = $value[$name];
        }

        if (!isset($value['input'])) {
            $this->_error(self::MISSING_VALUE);
            return false;
        }
        $input = strtolower($value['input']);
        $this->_setValue($input);

        if (!isset($value['id'])) {
            $this->_error(self::MISSING_ID);
            return false;
        }

        $this->_id = $value['id'];
        if ($input !== $this->getWord()) {
            $this->_error(self::BAD_CAPTCHA);
            return false;
        }

        return true;
    }

    /**
     * Get captcha decorator
     *
     * @return string
     */
    public function getDecorator()
    {
        return "Captcha_Word";
    }
}
