<?php

namespace Croogo\Acl\Controller\Component\Auth;

use Cake\Auth\BaseAuthenticate;
use Cake\Controller\ComponentRegistry;
use Cake\Core\Exception\Exception;
use Cake\Http\Response;
use Cake\Http\ServerRequest;

/**
 * An authentication adapter for AuthComponent.  Provides the ability to authenticate using Token
 *
 * {{{
 *  $this->Auth->authenticate = array(
 *      'Authenticate.Token' => array(
 *          'fields' => array(
 *              'username' => 'username',
 *              'password' => 'password',
 *              'token' => 'public_key',
 *          ),
 *          'parameter' => '_token',
 *          'header' => 'X-MyApiTokenHeader',
 *          'userModel' => 'User',
 *          'scope' => array('User.active' => 1)
 *      )
 *  )
 * }}}
 *
 * @package     Croogo.Acl.Controller.Component.Auth
 */
class TokenAuthenticate extends BaseAuthenticate
{
    /**
     * Settings for this object.
     *
     * - `fields` The fields to use to identify a user by. Make sure `'token'` has been added to the array
     * - `parameter` The url parameter name of the token.
     * - `header` The token header value.
     * - `userModel` The model name of the User, defaults to User.
     * - `scope` Additional conditions to use when looking up and authenticating users,
     *    i.e. `array('User.is_active' => 1).`
     * - `recursive` The value of the recursive key passed to find(). Defaults to 0.
     * - `contain` Extra models to contain and store in session.
     *
     * @var array
     */
    public $settings = [
        'fields' => [
            'username' => 'username',
            'password' => 'password',
            'token' => 'token',
        ],
        'parameter' => '_token',
        'header' => 'X-ApiToken',
        'userModel' => 'User',
        'scope' => [],
        'recursive' => 0,
        'contain' => null,
    ];

    /**
     * Constructor
     *
     * @param ComponentRegistry $collection The Component collection used on this request.
     * @param array $settings Array of settings to use.
     * @throws Exception
     */
    public function __construct(ComponentRegistry $collection, $settings)
    {
        parent::__construct($collection, $settings);
        if (empty($this->settings['parameter']) && empty($this->settings['header'])) {
            throw new Exception(__d('croogo', 'You need to specify token parameter and/or header'));
        }
    }

    /**
     *
     * @param Request $request The request object
     * @param Response $response response object.
     * @return mixed.  False on login failure.  An array of User data on success.
     */
    public function authenticate(ServerRequest $request, Response $response)
    {
        $user = $this->getUser($request);
        if (!$user) {
            $response->statusCode(401);
            $response->send();
        }

        return $user;
    }

    /**
     * Get token information from the request.
     *
     * @param Request $request Request object.
     * @return mixed Either false or an array of user information
     */
    public function getUser(ServerRequest $request)
    {
        if (!empty($this->settings['header'])) {
            $token = $request->header($this->settings['header']);
            if ($token) {
                return $this->_findUser($token, null);
            }
        }
        if (!empty($this->settings['parameter']) && !empty($request->query[$this->settings['parameter']])) {
            $token = $request->query[$this->settings['parameter']];

            return $this->_findUser($token);
        }

        return false;
    }

    /**
     * Find a user record.
     *
     * @param string $username The token identifier.
     * @param string $password Unused password.
     * @return array|bool Either false on failure, or an array of user data.
     */
    protected function _findUser($username, $password = null)
    {
        $userModel = $this->settings['userModel'];
        list($plugin, $model) = pluginSplit($userModel);
        $fields = $this->settings['fields'];

        $conditions = [
            $model . '.' . $fields['token'] => $username,
        ];
        if (!empty($this->settings['scope'])) {
            $conditions = array_merge($conditions, $this->settings['scope']);
        }
        $result = ClassRegistry::init($userModel)->find('first', [
            'conditions' => $conditions,
            'recursive' => (int)$this->settings['recursive'],
            'contain' => $this->settings['contain'],
        ]);
        if (empty($result) || empty($result[$model])) {
            return false;
        }
        $user = $result[$model];
        unset($user[$fields['password']]);
        unset($result[$model]);

        return array_merge($user, $result);
    }
}
