<?php

/**
 * @package     Joomla.Plugin
 * @subpackage  System.Webauthn
 *
 * @copyright   (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\Plugin\System\Webauthn\PluginTraits;

use Joomla\CMS\Factory;
use Joomla\CMS\Form\Form;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserFactoryInterface;
use Joomla\Event\Event;
use Joomla\Plugin\System\Webauthn\Extension\Webauthn;
use Joomla\Registry\Registry;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Add extra fields in the User Profile page.
 *
 * This class only injects the custom form fields. The actual interface is rendered through
 * JFormFieldWebauthn.
 *
 * @see     JFormFieldWebauthn::getInput()
 *
 * @since   4.0.0
 */
trait UserProfileFields
{
    /**
     * User object derived from the displayed user profile data.
     *
     * This is required to display the number and names of authenticators already registered when
     * the user displays the profile view page.
     *
     * @var   User|null
     * @since 4.0.0
     */
    private static $userFromFormData = null;

    /**
     * HTMLHelper method to render the WebAuthn user profile field in the profile view page.
     *
     * Instead of showing a nonsensical "Website default" label next to the field, this method
     * displays the number and names of authenticators already registered by the user.
     *
     * This static method is set up for use in the onContentPrepareData method of this plugin.
     *
     * @param   mixed  $value  Ignored. The WebAuthn profile field is virtual, it doesn't have a
     *                         stored value. We only use it as a proxy to render a sub-form.
     *
     * @return  string
     * @since   4.0.0
     */
    public static function renderWebauthnProfileField($value): string
    {
        if (\is_null(self::$userFromFormData)) {
            return '';
        }

        /** @var Webauthn $plugin */
        $plugin               = Factory::getApplication()->bootPlugin('webauthn', 'system');
        $credentialRepository = $plugin->getAuthenticationHelper()->getCredentialsRepository();
        $credentials          = $credentialRepository->getAll(self::$userFromFormData->id);
        $authenticators       = array_map(
            function (array $credential) {
                return $credential['label'];
            },
            $credentials
        );

        return Text::plural('PLG_SYSTEM_WEBAUTHN_FIELD_N_AUTHENTICATORS_REGISTERED', \count($authenticators), implode(', ', $authenticators));
    }

    /**
     * Adds additional fields to the user editing form
     *
     * @param   Event  $event  The event we are handling
     *
     * @return  void
     *
     * @throws  \Exception
     * @since   4.0.0
     */
    public function onContentPrepareForm(Event $event)
    {
        /**
         * @var   Form  $form The form to be altered.
         * @var   mixed $data The associated data for the form.
         */
        [$form, $data] = array_values($event->getArguments());

        $name = $form->getName();

        $allowedForms = [
            'com_admin.profile', 'com_users.user', 'com_users.profile', 'com_users.registration',
        ];

        if (!\in_array($name, $allowedForms)) {
            return;
        }

        // This feature only applies in the site and administrator applications
        if (
            !$this->getApplication()->isClient('site')
            && !$this->getApplication()->isClient('administrator')
        ) {
            return;
        }

        // This feature only applies to HTTPS sites.
        if (!Uri::getInstance()->isSsl()) {
            return;
        }

        // Get the user object
        $user = $this->getUserFromData($data);

        // Make sure the loaded user is the correct one
        if (\is_null($user)) {
            return;
        }

        // Make sure I am either editing myself OR I am a Super User
        if (!$this->canEditUser($user)) {
            return;
        }

        // Add the fields to the form.
        if ($name !== 'com_users.registration') {
            Log::add('Injecting WebAuthn Passwordless Login fields in user profile edit page', Log::DEBUG, 'webauthn.system');

            Form::addFormPath(JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/forms');
            $form->loadFile('webauthn', false);
        }
    }

    /**
     * @param   Event  $event  The event we are handling
     *
     * @return  void
     *
     * @throws  \Exception
     * @since   4.0.0
     */
    public function onContentPrepareData(Event $event): void
    {
        /**
         * @var   string|null        $context  The context for the data
         * @var   array|object|null  $data     An object or array containing the data for the form.
         */
        [$context, $data] = array_values($event->getArguments());

        if (!\in_array($context, ['com_users.profile', 'com_users.user'])) {
            return;
        }

        self::$userFromFormData = $this->getUserFromData($data);

        if (!HTMLHelper::isRegistered('users.webauthnWebauthn')) {
            HTMLHelper::register('users.webauthn', [__CLASS__, 'renderWebauthnProfileField']);
        }
    }

    /**
     * Get the user object based on the ID found in the provided user form data
     *
     * @param   array|object|null  $data  The user form data
     *
     * @return  User|null  A user object or null if no match is found
     *
     * @throws  \Exception
     * @since   4.0.0
     */
    private function getUserFromData($data): ?User
    {
        $id = null;

        if (\is_array($data)) {
            $id = $data['id'] ?? null;
        } elseif (\is_object($data) && ($data instanceof Registry)) {
            $id = $data->get('id');
        } elseif (\is_object($data)) {
            $id = $data->id ?? null;
        }

        $user = empty($id) ? Factory::getApplication()->getIdentity() : Factory::getContainer()
            ->get(UserFactoryInterface::class)
            ->loadUserById($id);

        // Make sure the loaded user is the correct one
        if ($user->id != $id) {
            return null;
        }

        return $user;
    }

    /**
     * Is the current user allowed to edit the WebAuthn configuration of $user?
     *
     * To do so I must either be editing my own account OR I have to be a Super User.
     *
     * @param   ?User   $user   The user you want to know if we're allowed to edit
     *
     * @return  boolean
     *
     * @since   4.2.0
     */
    private function canEditUser(?User $user = null): bool
    {
        // I can edit myself, but Guests can't have passwordless logins associated
        if (empty($user) || $user->guest) {
            return true;
        }

        // Get the currently logged in used
        $myUser = $this->getApplication()->getIdentity() ?? new User();

        // I can edit myself. If I'm a Super user I can edit other users too.
        return ($myUser->id == $user->id) || $myUser->authorise('core.admin');
    }
}
