<?php
declare(strict_types=1);

/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         1.0.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Authentication\Authenticator;

use Authentication\Identifier\AbstractIdentifier;
use Authentication\Identifier\IdentifierCollection;
use Authentication\Identifier\IdentifierInterface;
use Authentication\UrlChecker\UrlCheckerTrait;
use Cake\Routing\Router;
use Psr\Http\Message\ServerRequestInterface;

/**
 * Form Authenticator
 *
 * Authenticates an identity based on the POST data of the request.
 */
class FormAuthenticator extends AbstractAuthenticator
{
    use UrlCheckerTrait;

    /**
     * Default config for this object.
     * - `fields` The fields to use to identify a user by.
     * - `loginUrl` Login URL or an array of URLs.
     * - `urlChecker` Url checker config.
     *
     * @var array
     */
    protected array $_defaultConfig = [
        'loginUrl' => null,
        'urlChecker' => 'Authentication.Default',
        'fields' => [
            AbstractIdentifier::CREDENTIAL_USERNAME => 'username',
            AbstractIdentifier::CREDENTIAL_PASSWORD => 'password',
        ],
    ];

    /**
     * Gets the identifier, loading a default Password identifier if none configured.
     *
     * This is done lazily to allow loadIdentifier() to be called after loadAuthenticator().
     *
     * @return \Authentication\Identifier\IdentifierInterface
     */
    public function getIdentifier(): IdentifierInterface
    {
        if ($this->_identifier instanceof IdentifierCollection && $this->_identifier->isEmpty()) {
            $identifierConfig = [];
            if ($this->getConfig('fields')) {
                $identifierConfig['fields'] = $this->getConfig('fields');
            }
            $this->_identifier->load('Authentication.Password', $identifierConfig);
        }

        return $this->_identifier;
    }

    /**
     * Checks the fields to ensure they are supplied.
     *
     * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
     * @return array|null Username and password retrieved from a request body.
     */
    protected function _getData(ServerRequestInterface $request): ?array
    {
        $fields = $this->_config['fields'];
        /** @var array $body */
        $body = $request->getParsedBody();

        $data = [];
        foreach ($fields as $key => $field) {
            if (!isset($body[$field])) {
                return null;
            }

            $value = $body[$field];
            if (!is_string($value) || !strlen($value)) {
                return null;
            }

            $data[$key] = $value;
        }

        return $data;
    }

    /**
     * Prepares the error object for a login URL error
     *
     * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
     * @return \Authentication\Authenticator\ResultInterface
     */
    protected function _buildLoginUrlErrorResult(ServerRequestInterface $request): ResultInterface
    {
        $uri = $request->getUri();
        $base = $request->getAttribute('base');
        if ($base !== null) {
            $uri = $uri->withPath((string)$base . $uri->getPath());
        }

        $checkFullUrl = $this->getConfig('urlChecker.checkFullUrl', false);
        if ($checkFullUrl) {
            $uri = (string)$uri;
        } else {
            $uri = $uri->getPath();
        }

        $loginUrls = (array)$this->getConfig('loginUrl');
        foreach ($loginUrls as $key => $loginUrl) {
            if (is_array($loginUrl)) {
                $loginUrls[$key] = Router::url($loginUrl);
            }
        }

        $errors = [
            sprintf(
                'Login URL `%s` did not match `%s`.',
                $uri,
                implode('` or `', $loginUrls),
            ),
        ];

        return new Result(null, Result::FAILURE_OTHER, $errors);
    }

    /**
     * Authenticates the identity contained in a request.
     *
     * Will use the `config.userModel`, and `config.fields` to find POST data
     * that is used to find a matching record in the `config.userModel`.
     * Will return false if there is no post data, either username or password is missing,
     * or if the scope conditions have not been met.
     *
     * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
     * @return \Authentication\Authenticator\ResultInterface
     */
    public function authenticate(ServerRequestInterface $request): ResultInterface
    {
        if (!$this->_checkUrl($request)) {
            return $this->_buildLoginUrlErrorResult($request);
        }

        $data = $this->_getData($request);
        if ($data === null) {
            return new Result(null, Result::FAILURE_CREDENTIALS_MISSING, [
                'Login credentials not found',
            ]);
        }

        $identifier = $this->getIdentifier();
        $user = $identifier->identify($data);

        if (!$user) {
            return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $identifier->getErrors());
        }

        return new Result($user, Result::SUCCESS);
    }
}
