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

namespace Akeeba\Component\AdminTools\Administrator\Model;

defined('_JEXEC') or die;

use Akeeba\Component\AdminTools\Administrator\Mixin\RunPluginsTrait;
use Akeeba\Component\AdminTools\Administrator\Mixin\TableNoSuperUsersCheckFlagsTrait;
use Akeeba\Component\AdminTools\Administrator\Mixin\TempSuperUserChecksTrait;
use Exception;
use Joomla\CMS\Access\Access;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormFactoryInterface;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\AdminModel;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserFactoryInterface;
use Joomla\CMS\User\UserHelper;
use RuntimeException;

#[\AllowDynamicProperties]
class TempsuperuserModel extends AdminModel
{
	use TableNoSuperUsersCheckFlagsTrait;
	use TempSuperUserChecksTrait;
	use RunPluginsTrait;

	/**
	 * Cache of user group IDs with Super User privileges
	 *
	 * @var   array
	 * @since 5.3.0
	 */
	protected $superUserGroups = [];

	public function __construct($config = [], MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null)
	{
		parent::__construct($config, $factory, $formFactory);

		$this->_parent_table = '';
	}

	/**
	 * @inheritDoc
	 */
	public function getForm($data = [], $loadData = true)
	{
		$pk       = (int) $this->getState($this->getName() . '.id');
		$id       = $data['user_id'] ?? $pk;
		$user     = empty($id) ? new User() : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($id);
		$formName = (!empty($id) && ($user->id == $id)) ? 'tempsuperuser' : 'tempsuperuser_wizard';

		$form = $this->loadForm(
			'com_admintools.' . $formName,
			$formName,
			[
				'control'   => 'jform',
				'load_data' => $loadData,
			]
		) ?: false;

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

		return $form;
	}

	public function save($data)
	{
		try
		{
			$this->setNoCheckFlags(true);

			$isNew = empty($data['user_id'] ?? null);

			$data['user_id'] = ($data['user_id'] ?? null) ?: $this->getUserFromData($data);
		}
		catch (Exception $e)
		{
			$this->setError($e->getMessage());

			return false;
		}
		finally
		{
			$this->setNoCheckFlags(false);
		}

		try
		{
			$table   = $this->getTable();
			$context = $this->option . '.' . $this->name;
			$app     = Factory::getApplication();

			$key = $table->getKeyName();
			$pk  = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id');

			if (!$isNew)
			{
				$table->load($pk);
			}

			// Bind the data.
			if (!$table->bind($data))
			{
				$this->setError($table->getError());

				return false;
			}

			// Prepare the row for saving
			$this->prepareTable($table);

			// Check the data.
			if (!$table->check())
			{
				$this->setError($table->getError());

				return false;
			}

			// Trigger the before save event.
			$result = $this->triggerPluginEvent($this->event_before_save, [$context, $table, $isNew, $data], null, $app);

			if (\in_array(false, $result, true))
			{
				$this->setError($table->getError());

				return false;
			}

			// Store the data.
			try
			{
				if ($isNew)
				{
					$this->getDatabase()->insertObject($table->getTableName(), $table, $table->getKeyName());
				}
				else
				{
					$this->getDatabase()->updateObject($table->getTableName(), $table, $table->getKeyName(), true);
				}
			}
			catch (Exception $e)
			{
				$this->setError($e->getMessage());

				return false;
			}

			if (!$table->store())
			{
				$this->setError($table->getError());

				return false;
			}

			// Clean the cache.
			$this->cleanCache();

			// Trigger the after save event.
			$this->triggerPluginEvent($this->event_after_save, [$context, $table, $isNew, $data], null, $app);
		}
		catch (Exception $e)
		{
			$this->setError($e->getMessage());

			return false;
		}

		if (isset($table->$key))
		{
			$this->setState($this->getName() . '.id', $table->$key);
		}

		$this->setState($this->getName() . '.new', $isNew);

		return true;
	}

	protected function canDelete($record)
	{
		$this->assertNotMyself($record->id);

		return parent::canDelete($record);
	}

	/**
	 * Loads the form data.
	 *
	 * This method has three modes of operation:
	 *
	 * - If there is saved form data in the session and the user_id (PK) matches we'll use that.
	 * - If we are editing an existing record we load the record.
	 * - Otherwise it's the wizard layout and we pre-fill it with some sane defaults.
	 *
	 * @return array|bool|\Joomla\CMS\Object\CMSObject|mixed
	 * @throws Exception
	 */
	protected function loadFormData()
	{
		/** @var CMSApplication $app */
		$app  = Factory::getApplication();
		$data = $app->getUserState('com_admintools.edit.tempsuperuser.data', []);
		$pk   = (int) $this->getState($this->getName() . '.id');
		$item = ($pk ? (object) $this->getItem()->getProperties() : false) ?: [];

		// There's data saved in the session and the user_id in it matches what we're editing
		if (!empty($data) && ($item->user_id ?? null) == ($data['user_id'] ?? null))
		{
			$this->preprocessData('com_admintools.tempsuperuser', $data);

			return $data;
		}

		// First, let's try loading an existing item.
		$data = $item;
		$pk   = (int) $this->getState($this->getName() . '.id');

		if ($pk > 0)
		{
			$this->preprocessData('com_admintools.tempsuperuser', $data);

			return $data;
		}

		// No existing item. I am in the Wizard view. Let's preload it with some randomized, default values.
		$jDate           = clone Factory::getDate();
		$interval        = new \DateInterval('P15D');
		$superUserGroups = $this->getSuperUserGroups() ?: [8];

		// Get a random password respecting Joomla's password restrictions
		$uParams  = ComponentHelper::getParams('com_users');
		$length   = max($uParams->get('minimum_length', 8) ?: 8, 32);
		$nInt     = $uParams->get('minimum_integers', 0) ?: 0;
		$nSymbols = $uParams->get('minimum_symbols', 0) ?: 0;
		$nUpper   = $uParams->get('minimum_uppercase', 0) ?: 0;
		$nLower   = $uParams->get('minimum_lowercase', 0) ?: 0;
		$password = $this->generatePassword($length, $nInt, $nSymbols, $nUpper, $nLower);

		return [
			'expiration' => $jDate->add($interval)->toRFC822(),
			'username'   => 'temp' . UserHelper::genRandomPassword(12),
			'password'   => $password,
			'password2'  => $password,
			'email'      => UserHelper::genRandomPassword(12) . '@example.com',
			'name'       => Text::_('COM_ADMINTOOLS_TEMPSUPERUSERS_LBL_DEFAULTNAME'),
			'groups'     => array_shift($superUserGroups),
		];
	}

	/**
	 * Generate a random password with specific restrictions
	 *
	 * @param   int       $length     Password length
	 * @param   int|null  $minInt     Minimum number of integers to include
	 * @param   int|null  $minSymbol  Minimum number of symbols to include
	 * @param   int|null  $minUpper   Minimum number of uppercase English letters to include
	 * @param   int|null  $minLower   Minimum number of lowercase English letters to include
	 *
	 * @return  string  A secure, random password
	 * @throws  Exception
	 */
	private function generatePassword(int $length = 64, ?int $minInt = 0, ?int $minSymbol = 0, ?int $minUpper = 0, ?int $minLower = 0): string
	{
		$sNumbers    = '1234567890';
		$sSymbols    = '~!@#$%^&*()_+[]{};:\'"\|,<.>/?';
		$sUpper      = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
		$sLower      = 'abcdefghijklmnopqrstuvwxyz';
		$sEverything = $sLower . $sUpper . $sNumbers . $sSymbols;

		$getRandomCharacters = function (int $length, string $salt = '') use ($sEverything) {
			$salt     = $salt ?: $sEverything;
			$base     = \strlen($salt);
			$makepass = '';
			$random   = random_bytes($length + 1);
			$shift    = \ord($random[0]);

			for ($i = 1; $i <= $length; ++$i)
			{
				$makepass .= $salt[($shift + \ord($random[$i])) % $base];
				$shift    += \ord($random[$i]);
			}

			return $makepass;
		};

		// The password length is at least the sum of the minimum occurrences set up
		$minLength = ($minInt ?? 0) + ($minSymbol ?? 0) + ($minUpper ?? 0) + ($minLower ?? 0);
		$length    = max($length, $minLength);
		$pass      = '';

		// If there are no requirements on minimum number of characters return a truly random password and be done with it
		if ($minLength === 0)
		{
			return $getRandomCharacters($length, $sUpper . $sLower . $sNumbers);
		}

		// Create a minimum number of integers
		if (($minInt ?? 0) > 0)
		{
			$pass .= $getRandomCharacters($minInt, $sNumbers);
		}

		// Create a minimum number of symbols
		if (($minSymbol ?? 0) > 0)
		{
			$pass .= $getRandomCharacters($minSymbol, $sSymbols);
		}

		// Create a minimum number of uppercase characters
		if (($minUpper ?? 0) > 0)
		{
			$pass .= $getRandomCharacters($minUpper, $sUpper);
		}

		// Create a minimum number of lowercase characters
		if (($minLower ?? 0) > 0)
		{
			$pass .= $getRandomCharacters($minUpper, $sLower);
		}

		// Add random characters for the remaining length of the password
		$remainingLength = $length - $minLength + 1;

		if ($remainingLength > 0)
		{
			$pass .= $getRandomCharacters($remainingLength, $sEverything);
		}

		// Shuffle the characters
		for ($i = 0; $i < strlen($sEverything) * strlen($pass); $i++)
		{
			$from        = random_int(0, $length - 1);
			$to          = random_int(0, $length - 1);
			$temp        = $pass[$to];
			$pass[$to]   = $pass[$from];
			$pass[$from] = $temp;
		}

		return $pass;
	}

	/**
	 * Returns all Joomla! user groups
	 *
	 * @return  array
	 *
	 * @since   5.3.0
	 */
	private function getSuperUserGroups()
	{
		if (!empty($this->superUserGroups))
		{
			return $this->superUserGroups;
		}

		// Get all groups
		$db    = $this->getDatabase();
		$query = $db->getQuery(true)
			->select([$db->qn('id')])
			->from($db->qn('#__usergroups'));

		$this->superUserGroups = $db->setQuery($query)->loadColumn(0);

		// This should never happen (unless your site is very dead, in which case I feel terribly sorry for you...)
		if (empty($this->superUserGroups))
		{
			$this->superUserGroups = [];
		}

		$this->superUserGroups = array_filter($this->superUserGroups, function ($group) {
			return Access::checkGroup($group, 'core.admin');
		});

		return $this->superUserGroups;
	}

	private function getUserFromData($info)
	{
		$info['block']         = 0;
		$info['sendEmail']     = 0;
		$info['lastvisitDate'] = (clone Factory::getDate())->toSql();
		$info['activation']    = '';
		$info['otpKey']        = '';
		$info['otep']          = '';
		$info['requireReset']  = 0;

		$userId = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserByUsername($info['username'])->id;

		if (empty($userId))
		{
			return $this->createNewUser($info);
		}

		// Make sure I am not trying to edit myself
		if ($userId == Factory::getApplication()->getIdentity()->id)
		{
			throw new RuntimeException(Text::_('COM_ADMINTOOLS_TEMPSUPERUSERS_ERR_CANTEDITSELF'), 403);
		}

		// Get the existing user
		$user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId);

		// Make sure the user is a Super User
		if (!$user->authorise('core.admin'))
		{
			throw new RuntimeException(Text::_('COM_ADMINTOOLS_TEMPSUPERUSERS_ERR_NOTSUPER'), 500);
		}

		// Make sure the user was already blocked
		if (!$user->block)
		{
			throw new RuntimeException(Text::_('COM_ADMINTOOLS_TEMPSUPERUSERS_ERR_NOTBLOCKED'), 500);
		}

		// Apply changes to the existing user
		$user->bind($info);

		$saved = $user->save();

		if (!$saved)
		{
			throw new RuntimeException($user->getError());
		}

		return $userId;
	}

	private function createNewUser(&$info)
	{
		// Make sure $info['groups'] is defined and defines at least one Super User group
		$superUserGroups = $this->getSuperUserGroups();
		$usedSUGroups    = array_intersect($info['groups'], $superUserGroups);

		if (empty($usedSUGroups))
		{
			$this->setNoCheckFlags(false);

			throw new RuntimeException(Text::_('COM_ADMINTOOLS_TEMPSUPERUSERS_ERR_NOTASUPERUSER'), 500);
		}

		// Create a new user
		$user = new User();

		// Set the user's default language to whatever the site's current language is
		$info['params'] = [
			'language' => Factory::getApplication()->get('language'),
		];

		$user->bind($info);

		$saved = $user->save();

		if (!$saved)
		{
			$this->setNoCheckFlags(false);

			throw new RuntimeException($user->getError());
		}

		$this->addUserToSafeId($user->id);

		return $user->id;
	}

	/**
	 * Adds a new user into the list of "safe ids", otherwise at the next session load it will be disabled by the
	 * feature "Monitor Super User accounts"
	 *
	 * @param   int  $userid  ID of the new user that should be injected into the list
	 */
	private function addUserToSafeId($userid)
	{
		$db    = $this->getDatabase();
		$query = $db->getQuery(true)
			->select($db->quoteName('value'))
			->from($db->quoteName('#__admintools_storage'))
			->where($db->quoteName('key') . ' = ' . $db->quote('superuserslist'));
		$db->setQuery($query);

		try
		{
			$jsonData = $db->loadResult();
		}
		catch (Exception $e)
		{
			return;
		}

		$userList = [];

		if (!empty($jsonData))
		{
			$userList = json_decode($jsonData, true);
		}

		$userList[] = $userid;

		$data = json_encode($userList);

		$query = $db->getQuery(true)
			->delete($db->quoteName('#__admintools_storage'))
			->where($db->quoteName('key') . ' = ' . $db->quote('superuserslist'));
		$db->setQuery($query);
		$db->execute();

		$object = (object) [
			'key'   => 'superuserslist',
			'value' => $data,
		];

		$db->insertObject('#__admintools_storage', $object);
	}

}

© 2025 Cubjrnet7