<?php

namespace Croogo\Acl\Auth;

use Cake\Auth\BaseAuthenticate;
use Cake\Core\Configure;
use Cake\Core\Exception\Exception;
use Cake\Http\Response;
use Cake\Http\ServerRequest;
use Cake\ORM\TableRegistry;

/**
 * An authentication adapter for AuthComponent.  Provides the ability to authenticate using COOKIE
 *
 * {{{
 *  $this->Auth->authenticate = array(
 *      'Authenticate.Cookie' => array(
 *          'fields' => array(
 *              'username' => 'username',
 *              'password' => 'password'
 *          ),
 *          'userModel' => 'User',
 *          'scope' => array('User.active' => 1),
 *          'crypt' => 'rijndael', // Defaults to rijndael(safest), optionally set to 'cipher' if required
 *          'cookie' => array(
 *              'name' => 'RememberMe',
 *              'time' => '+2 weeks',
 *          )
 *      )
 *  )
 * }}}
 *
 * @package     Croogo.Acl.Auth
 * @copyright   Copyright (c) 2012 Ceeram
 * @license     http://www.opensource.org/licenses/mit-license.php The MIT License
 * @see AclAutoLoginComponent
 */
class CookieAuthenticate extends BaseAuthenticate
{

    /**
     * Cookie configuration
     *
     * FIXME: This config should be centralized/shared with AutoLoginComponent
     */
    protected $_defaultConfig = [
        'cookie' => [
            'name' => 'CAL',
            'time' => '+2 weeks',
        ],
    ];

    /**
     * Verify cookie data
     *
     * return boolean|array User data or boolean False when data is invalid
     */
    protected function _verify($cookie)
    {
        if (empty($cookie['data'])) {
            return false;
        }

        $data = $cookie['data'];
        $mac = hash_hmac('sha256', $data, Configure::read('Security.salt'));
        if ($mac !== $cookie['mac']) {
            return false;
        }

        $data = json_decode($cookie['data'], true);
        $settings = $this->getConfig();
        $fields = $settings['fields'];
        if (empty($data['hash']) ||
            empty($data['time']) ||
            empty($data[$fields['username']])
        ) {
            return false;
        }

        $username = $data[$fields['username']] . $data['time'];
        if ($this->passwordHasher()->check($username, $data['hash'])) {
            return $data;
        }

        return false;
    }

    /**
     * Authenticates the identity contained in the cookie.  Will use the `settings.userModel`, and `settings.fields`
     * to find COOKIE data that is used to find a matching record in the `settings.userModel`.  Will return false if
     * there is no cookie data, either username or password is missing, of if the scope conditions have not been met.
     *
     * @param Request $request The unused request object
     * @return mixed False on login failure. An array of User data on success.
     * @throws Exception
     */
    public function getUser(ServerRequest $request)
    {
        if (!$this->_registry->has('Cookie')) {
            throw new Exception('CookieComponent is not loaded');
        }

        $config = $this->getConfig();
        if (!function_exists('mcrypt_encrypt') && !function_exists('openssl_encrypt')) {
            throw new Exception('Cannot use encryption, either mcrypt_encrypt() or openssl_encrypt() is required');
        }

        list(, $model) = pluginSplit($config['userModel']);

        $cookieName = $config['cookie']['name'];
        unset($config['cookie']['name']);
        $this->_registry->Cookie->configKey($cookieName, [
            'httpOnly' => true,
        ]);
        $cookie = $this->_registry->Cookie->read($cookieName);
        $data = $this->_verify($cookie);
        if (!$data) {
            return false;
        }

        extract($config['fields']);
        if (empty($data[$username])) {
            return false;
        }

        $user = $this->_findUser($data[$username]);
        if ($user) {
            $this->_registry->Auth->setUser($user);

            return $user;
        }

        return false;
    }

    /**
     * Find a user record
     *
     * @see BaseAuthenticate::_findUser()
     */
    protected function _findUser($conditions, $password = null)
    {
        $config = $this->getConfig();
        $userModel = $config['userModel'];
        list(, $model) = pluginSplit($userModel);
        $fields = $config['fields'];

        if (!is_array($conditions)) {
            $username = $conditions;
            $conditions = [
                $model . '.' . $fields['username'] => $username,
            ];
        }
        if (!empty($this->settings['scope'])) {
            $conditions = array_merge($conditions, $this->settings['scope']);
        }

        $query = TableRegistry::get($userModel)->find()
            ->where($conditions);

        if (!empty($config['contain'])) {
            $query->contain($config['contain']);
        }

        $user = $query->first()->toArray();
        if (empty($user) || empty($user[$fields['username']])) {
            return false;
        }
        if (isset($conditions[$model . '.' . $fields['password']]) ||
            isset($conditions[$fields['password']])
        ) {
            unset($user[$fields['password']]);
        }

        return $user;
    }

    /**
     * Authenticate a user based on the request information
     *
     * @see BaseAuthenticate::authenticate()
     */
    public function authenticate(ServerRequest $request, Response $response)
    {
        if ($request->getData() || $request->is('post')) {
            return false;
        }

        return $this->getUser($request);
    }
}
