name : AdminpasswordModel.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 DateTimeZone;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\MVC\Model\BaseModel;
use Joomla\CMS\User\UserHelper;

#[\AllowDynamicProperties]
class AdminpasswordModel extends BaseModel
{
	use ApacheVersionTrait;

	/**
	 * Applies the back-end protection.
	 *
	 * Creates the necessary .htaccess and .htpasswd files in the administrator directory.
	 *
	 * @return  bool
	 */
	public function protect(): bool
	{
		$cryptpw      = $this->apacheEncryptPassword();
		$htpasswd     = $this->getState('username') . ':' . $cryptpw . "\n";
		$htpasswdPath = JPATH_ADMINISTRATOR . '/.htpasswd';
		$htaccessPath = $this->getPublicAdminFolder() . '/.htaccess';

		if (!File::write($htpasswdPath, $htpasswd))
		{
			return false;
		}

		switch ($this->getState('mode', 'everything'))
		{
			default:
			case 'everything':
				$mode       = "Everything";
				$comment    = "Enable password protection for all resources in this directory and its subdirectories";
				$wrapBefore = '';
				$wrapAfter  = '';
				break;

			case 'joomla':
				$mode       = "Joomla";
				$comment    = "Enable password protection only for Joomla's index.php file in this directory";
				$wrapBefore = '<FilesMatch "^index\.php$">';
				$wrapAfter  = '</FilesMatch>';
				break;

			case 'php':
				$mode       = "All PHP Files";
				$comment    = "Enable password protection for all .php files in this directory and its subdirectories";
				$wrapBefore = '<FilesMatch "\.php$">';
				$wrapAfter  = '</FilesMatch>';
				break;
		}

		$app       = Factory::getApplication();
		$user      = $app->getIdentity();
		$path      = rtrim(JPATH_ADMINISTRATOR, '/\\') . '/';
		$date      = clone Factory::getDate();
		$tz        = new DateTimeZone($user->getParam('timezone', $app->get('offset', 'UTC')));
		$timestamp = $date->setTimezone($tz)->format('Y-m-d H:i:s T', true);
		$version   = ADMINTOOLS_VERSION;
		$htaccess  = <<<HTACCESS
########################################################################################################################
## Administrator Password Protection
##
## This file was generated by Admin Tools $version on $timestamp
##
## Password protection mode selected: $mode
##
## If you are unable to access your site's administrator OR see a browser login prompt in the frontend of your site
## please delete this file and the .htpasswd file in the same folder. 
########################################################################################################################

## $comment
$wrapBefore
<IfModule mod_auth_basic.c>
	AuthUserFile "{$path}.htpasswd"
	AuthName "Restricted Area"
	AuthType Basic
	Require valid-user
</IfModule>
$wrapAfter

## Forbid access to the .htpasswd file containing your (hashed) password
<FilesMatch "^\.ht">
	Require all denied
</FilesMatch>

HTACCESS;

		if ($this->getState('resetErrorPages', 1))
		{
			$htaccess .= <<< HTACCESS

## Reset custom error pages to default
#
# Prevents a 404 error when trying to access your site's administrator directory
#
# See https://www.akeeba.com/documentation/admin-tools-joomla/admin-pw-protection.html#id604127
#
ErrorDocument 401 "Unauthorized"
ErrorDocument 403 "Forbidden"

HTACCESS;

		}

		$status = @file_put_contents($htaccessPath, $htaccess);

		if (!$status)
		{
			$status = File::write($htaccessPath, $htaccess);
		}

		if (!$status || !is_file($path . '/.htpasswd'))
		{
			File::delete($htpasswdPath);

			return false;
		}

		return true;
	}

	/**
	 * Removes the administrator protection.
	 *
	 * Removes both the .htaccess and .htpasswd files from the administrator directory
	 *
	 * @return bool
	 */
	public function unprotect(): bool
	{
		$htaccessPath = $this->getPublicAdminFolder() . '/.htaccess';
		$htpasswdPath = JPATH_ADMINISTRATOR . '/.htpasswd';

		return File::delete($htaccessPath) && File::delete($htpasswdPath);
	}

	/**
	 * Is the administrator directory password protected?
	 *
	 * Returns true if both a .htpasswd and .htaccess file exist in the back-end
	 *
	 * @return bool
	 */
	public function isLocked(): bool
	{
		$htaccessPath = $this->getPublicAdminFolder() . '/.htaccess';
		$htpasswdPath = JPATH_ADMINISTRATOR . '/.htpasswd';

		return @file_exists($htpasswdPath) && @file_exists($htaccessPath);
	}

	protected function populateState()
	{
		/** @var CMSApplication $app */
		$app = Factory::getApplication();

		$username = $app->getUserStateFromRequest('com_admintools.adminpassword.username', 'username', '', 'raw');
		$this->setState('username', $username);

		$password = $app->getUserStateFromRequest('com_admintools.adminpassword.password', 'password', '', 'raw');
		$this->setState('password', $password);

		$resetErrorPages = $app->getUserStateFromRequest(
			'com_admintools.adminpassword.resetErrorPages', 'resetErrorPages', 1, 'int'
		);
		$this->setState('resetErrorPages', $resetErrorPages);

		$mode = $app->getUserStateFromRequest('com_admintools.adminpassword.mode', 'mode', 'everything', 'cmd');
		$this->setState('mode', $mode);
	}

	protected function apacheEncryptPassword()
	{
		$os        = strtoupper(PHP_OS);
		$isWindows = substr($os, 0, 3) == 'WIN';

		// If this looks like Apache 2.4 we'll use bCrypt instead of legacy password protection
		$isApache24 = version_compare($this->apacheVersion(), '2.4', 'ge');

		if ($isApache24 && function_exists('password_hash') && defined('PASSWORD_BCRYPT'))
		{
			return password_hash($this->getState('password'), PASSWORD_BCRYPT);
		}

		// Iterated and salted MD5 (APR1)
		$salt              = UserHelper::genRandomPassword(4);
		$encryptedPassword = $this->apr1_hash($this->getState('password'), $salt, 1000);

		// SHA-1 encrypted – should never run
		if (empty($encryptedPassword) && function_exists('base64_encode') && function_exists('sha1'))
		{
			$encryptedPassword = '{SHA}' . base64_encode(sha1($this->getState('password'), true));
		}

		// Traditional crypt(3) – should never run
		if (empty($encryptedPassword) && function_exists('crypt') && !$isWindows)
		{
			$salt              = UserHelper::genRandomPassword(2);
			$encryptedPassword = crypt($this->getState('password'), $salt);
		}

		// Plain text fallback (should only happen on REALLY old PHP versions incompatible with Joomla)
		if (empty($encryptedPassword))
		{
			$encryptedPassword = $this->getState('password');
		}

		return $encryptedPassword;
	}

	/**
	 * Perform the hashing of the password
	 *
	 * @param   string  $password    The plain text password to hash
	 * @param   string  $salt        The 8 byte salt to use
	 * @param   int     $iterations  The number of iterations to use
	 *
	 * @return  string  The hashed password
	 */
	protected function apr1_hash($password, $salt, $iterations)
	{
		$len  = strlen($password);
		$text = $password . '$apr1$' . $salt;
		$bin  = md5($password . $salt . $password, true);

		for ($i = $len; $i > 0; $i -= 16)
		{
			$text .= substr($bin, 0, min(16, $i));
		}

		for ($i = $len; $i > 0; $i >>= 1)
		{
			$text .= ($i & 1) ? chr(0) : $password[0];
		}

		$bin = $this->apr1_iterate($text, $iterations, $salt, $password);

		return $this->apr1_convertToHash($bin, $salt);
	}

	protected function apr1_iterate($text, $iterations, $salt, $password)
	{
		$bin = md5($text, true);

		for ($i = 0; $i < $iterations; $i++)
		{
			$new = ($i & 1) ? $password : $bin;

			if ($i % 3)
			{
				$new .= $salt;
			}

			if ($i % 7)
			{
				$new .= $password;
			}

			$new .= ($i & 1) ? $bin : $password;
			$bin = md5($new, true);
		}

		return $bin;
	}

	protected function apr1_convertToHash($bin, $salt)
	{
		$tmp = '$apr1$' . $salt . '$';

		$tmp .= $this->apr1_to64(
			(ord($bin[0]) << 16) | (ord($bin[6]) << 8) | ord($bin[12]),
			4
		);

		$tmp .= $this->apr1_to64(
			(ord($bin[1]) << 16) | (ord($bin[7]) << 8) | ord($bin[13]),
			4
		);

		$tmp .= $this->apr1_to64(
			(ord($bin[2]) << 16) | (ord($bin[8]) << 8) | ord($bin[14]),
			4
		);

		$tmp .= $this->apr1_to64(
			(ord($bin[3]) << 16) | (ord($bin[9]) << 8) | ord($bin[15]),
			4
		);

		$tmp .= $this->apr1_to64(
			(ord($bin[4]) << 16) | (ord($bin[10]) << 8) | ord($bin[5]),
			4
		);

		$tmp .= $this->apr1_to64(
			ord($bin[11]),
			2
		);

		return $tmp;
	}

	/**
	 * Convert the input number to a base64 number of the specified size
	 *
	 * @param   int  $num   The number to convert
	 * @param   int  $size  The size of the result string
	 *
	 * @return  string  The converted representation
	 */
	protected function apr1_to64($num, $size)
	{
		static $seed = '';

		if (empty($seed))
		{
			$seed = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
		}

		$result = '';

		while (--$size >= 0)
		{
			$result .= $seed[$num & 0x3f];
			$num    >>= 6;
		}

		return $result;
	}

	/**
	 * Returns the absolute filesystem folder to the administrator directory.
	 *
	 * Joomla! 5 can be installed with a custom public folder. In this case the .htaccess file needs to be written in
	 * the public directory, whereas the .htpasswd file needs to be written in the regular administrator folder which is
	 * outside the public folder (web root).
	 *
	 * This method is here to get the public folder where the .htaccess file goes into.
	 *
	 * @return  string
	 *
	 * @since   7.4.3
	 */
	private function getPublicAdminFolder(): string
	{
		return !defined('JPATH_PUBLIC') ? JPATH_ADMINISTRATOR : (JPATH_PUBLIC . '/administrator');

	}
}

© 2025 Cubjrnet7