<?php
/**
 * @package   admintools
 * @copyright Copyright (c)2010-2026 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

namespace Akeeba\Plugin\System\AdminTools\Feature;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Language\Text;
use Joomla\CMS\User\User;
use Joomla\Http\HttpFactory;
use Joomla\Utilities\ArrayHelper;

class WarnAboutLeakedPasswords extends Base
{
	public function isEnabled()
	{
		// Protect vs broken host
		if (!function_exists('sha1'))
		{
			return false;
		}

		return ($this->wafParams->getValue('leakedpwd', 0) == 1);
	}

	/**
	 * Hooks into the Joomla! models before a user is saved.
	 *
	 * @param   User|array  $oldUser  The existing user record
	 * @param   bool        $isNew    Is this a new user?
	 * @param   array       $data     The data to be saved
	 *
	 * @throws  Exception  When we catch a security exception
	 */
	public function onUserBeforeSave($oldUser, $isNew, $data): bool
	{
		if (!isset($data['password_clear']) || !$data['password_clear'])
		{
			return true;
		}

		// This group is allowed to have insecure passwords? If so let's stop here
		if (!$this->checkByGroup($oldUser, $data))
		{
			return true;
		}

		// HIBP database searches for the first 5 chars, if the rest of the hash is in the response body, the password
		// is included in a leaked database
		$hashed = strtoupper(hash('sha1', $data['password_clear']));
		$search = substr($hashed, 0, 5);
		$body   = substr($hashed, 5);

		$http = (new HttpFactory())->getHttp();
		$http->setOption('user-agent', 'admin-tools-pwd-checker');

		try
		{
			$response = $http->get('https://api.pwnedpasswords.com/range/' . $search);
		}
		catch (Exception $e)
		{
			// Do not die if anything wrong happens
			return true;
		}

		// This should never happen, but better be safe than sorry
		if ($response->getStatusCode() !== 200)
		{
			return true;
		}

		// There's no need to further process the response: if the rest of the hash is inside the body,
		// it means that is an insecure password
		if (strpos((string) $response->getBody(), $body) !== false)
		{
			// Load the component's administrator translation files
			$jlang = $this->app->getLanguage();
			$jlang->load('com_admintools', JPATH_ADMINISTRATOR, 'en-GB', true);
			$jlang->load('com_admintools', JPATH_ADMINISTRATOR, $jlang->getDefault(), true);
			$jlang->load('com_admintools', JPATH_ADMINISTRATOR, null, true);

			throw new Exception(Text::sprintf('PLG_ADMINTOOLS_ERR_LEAKEDPWD', $data['password_clear']), '403');
		}

		return true;
	}

	/**
	 * Given a user group, should we allow insecure passwords?
	 *
	 * @param   User|array  $oldUser
	 * @param   array       $data
	 *
	 * @return bool        Should we continue with the check or no?
	 */
	private function checkByGroup($oldUser, $data)
	{
		$groups = $this->wafParams->getValue('leakedpwd_groups');

		if (!$groups)
		{
			return false;
		}

		if (!is_array($groups))
		{
			$groups = array_filter(ArrayHelper::toInteger(explode(',', $groups)));

			if (empty($groups))
			{
				return false;
			}
		}

		// Ok, here's the situation. If you DON'T change user group, $data['groups'] is empty and we have to check the $oldUser.
		// If you change the groups or create a new user, data['groups'] is populated and we have to check for it
		if (isset($data['groups']) && $data['groups'])
		{
			$user_groups = $data['groups'];
		}
		else
		{
			$user_groups = [];

			if (is_array($oldUser))
			{
				$user_groups = $oldUser['groups'];
			}
			elseif ($oldUser instanceof User)
			{
				$user_groups = $oldUser->groups;
			}
		}

		// We have the groups, now let's check them
		foreach ($user_groups as $user_group)
		{
			if (in_array($user_group, $groups))
			{
				return true;
			}
		}

		return false;
	}
}
