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

namespace Akeeba\Plugin\System\AdminTools\Feature;

defined('_JEXEC') || die;

use Joomla\CMS\Session\Session;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\UserHelper;
use Akeeba\Plugin\System\AdminTools\Utility\Filter;

/**
 * Allows users to "rename" their administrator directory. In fact, the "rename" is a smokes and mirrors trick,
 * manipulating Joomla!'s SEF routing to mask the administrator directory.
 */
class CustomAdminFolder extends Base
{
	/**
	 * Is this feature enabled?
	 *
	 * @return bool
	 */
	public function isEnabled()
	{
		$folder = $this->wafParams->getValue('adminlogindir');

		// Custom admin folder is disabled
		if (!$folder || !$this->app->get('sef') || !$this->app->get('sef_rewrite'))
		{
			return false;
		}

		return true;
	}

	/**
	 * Hooks to Joomla!'s earliest plugin handler
	 */
	public function onAfterInitialise(): void
	{
		$this->customAdminFolder();

		if ($this->isAdminAccessAttempt())
		{
			$this->checkCustomAdminFolder();
		}

		if ($this->isAdminLogout())
		{
			$this->setLogoutCookie();
		}
	}

	/**
	 * If the user is trying to access the custom admin folder set the necessary cookies and redirect them to the
	 * administrator page.
	 */
	protected function customAdminFolder()
	{
		$ip = Filter::getIp();

		// I couldn't detect the ip, let's stop here
		if (empty($ip) || ($ip == '0.0.0.0'))
		{
			return;
		}

		// Some user agents don't set a UA string at all
		if (!array_key_exists('HTTP_USER_AGENT', $_SERVER))
		{
			return;
		}

		$ua             = $this->app->client;
		$uaString       = $ua->userAgent;
		$browserVersion = $ua->browserVersion;

		$uaShort = str_replace($browserVersion, 'abcd', $uaString);

		$uri = Uri::getInstance();
		$db  = $this->db;

		// We're not trying to access to the custom folder
		$folder = $this->wafParams->getValue('adminlogindir');

		if (str_replace($uri->root(), '', trim($uri->current(), '/')) != $folder)
		{
			return;
		}

		$hash = UserHelper::hashPassword($ip . $uaShort);

		$data = (object) [
			'series'      => UserHelper::genRandomPassword(64),
			'client_hash' => $hash,
			'valid_to'    => date('Y-m-d H:i:s', time() + 180),
		];

		$db->insertObject('#__admintools_cookies', $data);

		$cookie_domain = $this->app->get('cookie_domain', '');
		$cookie_path   = $this->app->get('cookie_path', '/');
		$isSecure      = $this->app->get('force_ssl', 0) ? true : false;

		setcookie('admintools', $data->series, time() + 180, $cookie_path, $cookie_domain, $isSecure, true);
		setcookie('admintools_logout', null, 1, $cookie_path, $cookie_domain, $isSecure, true);

		$uri->setPath(str_replace($folder, 'administrator/index.php', $uri->getPath()));

		$this->app->redirect($uri->toString(), 307);
	}

	/**
	 * When the user is trying to access the administrator folder without being logged in make sure they had already
	 * entered the custom administrator folder before coming here. Otherwise they are unauthorised and must be booted to
	 * the site's front-end page.
	 */
	protected function checkCustomAdminFolder()
	{
		// Initialise
		$seriesFound = false;
		$db          = $this->db;

		// Get the series number from the cookie
		$series = $this->input->cookie->get('admintools', null);

		// If we are told that this is a user logging out redirect them to the front-end home page, do not log a
		// security exception, expire the cookie
		$logout = $this->input->cookie->get('admintools_logout', null, 'string');
		if ($logout == '!!!LOGOUT!!!')
		{
			$cookie_domain = $this->app->get('cookie_domain', '');
			$cookie_path   = $this->app->get('cookie_path', '/');
			$isSecure      = $this->app->get('force_ssl', 0) ? true : false;
			setcookie('admintools_logout', null, 1, $cookie_path, $cookie_domain, $isSecure, true);

			$this->redirectAdminToHome();

			return;
		}

		// Do we have a series?
		$isValid = !empty($series);

		// Does the series exist in the db? If so, load it
		if ($isValid)
		{
			$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
				->select('*')
				->from($db->qn('#__admintools_cookies'))
				->where($db->qn('series') . ' = ' . $db->q($series));
			$db->setQuery($query);
			$storedData = $db->loadObject();

			$seriesFound = true;

			if (!is_object($storedData))
			{
				$isValid     = false;
				$seriesFound = false;
			}
		}

		// Is the series still valid or did someone manipulate the cookie expiration?
		if ($isValid)
		{
			$jValid = strtotime($storedData->valid_to);

			if ($jValid < time())
			{
				$isValid = false;
			}
		}

		// Does the UA match the stored series?
		if ($isValid)
		{
			$ip = Filter::getIp();

			$ua             = $this->app->client;
			$uaString       = $ua->userAgent;
			$browserVersion = $ua->browserVersion;

			$uaShort = str_replace($browserVersion, 'abcd', $uaString);

			$notSoSecret = $ip . $uaShort;

			$isValid = UserHelper::verifyPassword($notSoSecret, $storedData->client_hash);
		}

		// Last check: session state variable
		if ($this->app->getSession()->get('com_admintools.adminlogindir', 0))
		{
			$isValid = true;
		}

		// Delete the series cookie if found
		if ($seriesFound)
		{
			$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
				->delete($db->qn('#__admintools_cookies'))
				->where($db->qn('series') . ' = ' . $db->q($series));
			$db->setQuery($query);
			$db->execute();
		}

		// Log an exception and redirect to homepage if we can't validate the user's cookie / session parameter
		if (!$isValid)
		{
			$this->exceptionsHandler->logRequest('admindir');

			$this->redirectAdminToHome();

			return;
		}

		// Otherwise set the session parameter
		if ($seriesFound)
		{
			$this->app->getSession()->set('com_admintools.adminlogindir', 1);
		}
	}

	protected function setLogoutCookie()
	{
		$cookie_domain = $this->app->get('cookie_domain', '');
		$cookie_path   = $this->app->get('cookie_path', '/');
		$isSecure      = $this->app->get('force_ssl', 0) ? true : false;

		setcookie('admintools_logout', '!!!LOGOUT!!!', time() + 180, $cookie_path, $cookie_domain, $isSecure, true);
	}

	/**
	 * Checks if a user is trying to log out
	 *
	 * @return bool
	 */
	protected function isAdminLogout()
	{
		// Not back-end at all. Bail out.
		if (!$this->app->isClient('administrator'))
		{
			return false;
		}

		// If the user is not already logged in we don't have a logout attempt
		$user = $this->app->getIdentity();

		if ($user->guest)
		{
			return false;
		}

		$input  = $this->input;
		$option = $input->getCmd('option', null);
		$task   = $input->getCmd('task', null);

		if (($option == 'com_login') && ($task == 'logout'))
		{
			return true;
		}

		// Check for malicious direct post without a valid token. In this case it's not a logout.
		$token = $this->app->getFormToken();
		$token = $this->input->get($token, false, 'raw');

		if ($token === false)
		{
			return Session::checkToken('request');
		}

		return false;
	}
}

© 2025 Cubjrnet7