| Current Path : /var/www/consult-e-syn/public_html/plugins/system/admintools/feature/ |
| Current File : /var/www/consult-e-syn/public_html/plugins/system/admintools/feature/disableobsoleteadmins.php |
<?php
/**
* @package admintools
* @copyright Copyright (c)2010-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
use FOF40\Date\Date;
use Joomla\CMS\Access\Access;
use Joomla\CMS\User\User;
defined('_JEXEC') || die;
/**
* Disable or force a password reset on obsolete administrators (backend users who have not logged into the site for a
* very long time)
*
* WAF configuration parameters:
* disableobsoleteadmins Is this feature enabled? Default: 0.
* disableobsoleteadmins_freq How often to run this feature [minutes]. Default: 60.
* disableobsoleteadmins_groups Which user groups to apply to? Default: empty (all groups)
* disableobsoleteadmins_maxdays Minimum time since last login to trigger this feature [days]. Default: 90
* disableobsoleteadmins_action Action to take (block|reset). Default: reset
* disableobsoleteadmins_protected Protected users
*
* @since 5.3.0
*/
class AtsystemFeatureDisableobsoleteadmins extends AtsystemFeatureAbstract
{
protected $loadOrder = 160;
/**
* WAF settings key prefix for this feature
*
* @var string
* @since 5.3.0
*/
protected $settingsKey = 'disableobsoleteadmins';
/**
* If the user has not logged in for at least this many days we are going to block / force reset their password.
*
* @var int
* @since 5.3.0
*/
protected $maxDays = 0;
/**
* When saving a user who was previously blocked, undoing the block, I have to update their last visit date to today
* so they don't get auto-blocked again. Joomla! does not let me modify the data before a user is saved to the
* database. What I do instead is intercept the onBeforeSave events, create a list of the users who need to be
* modified and then apply these changes by capturing the onUserAfterSave event for this user. The problem is that
* by doing so I am triggering yet again the onUserBeforeSave which would make me enter an infinite loop. This array
* lets me keep track of the user IDs I am fiddling with so I don't end up in an infinite loop. It's an array
* because due to the way user plugins work I *might* end up in a recursive update situation.
*
* @var array
* @since 5.3.0
*/
protected $toUpdateUsers = [];
/**
* This is part of the solution described in the $toUpdateUsers above. This array keeps track of the User IDs I have
* already started processing onUserAfterSave so I don't process them again.
*
* @var array
* @since 5.3.0
*/
protected $updatedUsers = [];
/**
* Cache of all the user groups known to Joomla
*
* @var array
* @since 5.3.0
*/
protected $allJoomlaUserGroups = [];
/**
* Is this feature enabled?
*
* @return bool
*
* @since 5.3.0
*/
public function isEnabled()
{
if ($this->cparams->getValue($this->settingsKey, 0) != 1)
{
return false;
}
$this->maxDays = $this->cparams->getValue($this->settingsKey . '_maxdays', 90);
if ($this->maxDays <= 0)
{
return false;
}
return true;
}
/**
* Runs as soon as the application has finished initializing, before it routes to a component. We will run our
* feature at most every disableobsoleteadmins_freq minutes (default: every 60 minutes)
*
* @throws Exception
*
* @since 5.3.0
*/
public function onAfterInitialise()
{
$minutes = $this->getRunFrequency();
$lastJob = $this->getTimestamp($this->settingsKey);
$nextJob = $lastJob + $minutes * 60;
$now = new Date();
if ($now->toUnix() >= $nextJob)
{
$this->setTimestamp($this->settingsKey);
$this->disableObsoleteAdmins();
}
}
/**
* Prevent automatic blocking of a backend user manually unblocked by an admin user.
*
* Presumably one of your users got blocked and they asked you to manually reset their password because they can't
* figure out the password reset instructions. If you edit them and remove the forced password reset / user block
* from their user account they will be automatically blocked again by this feature. This happens because their
* last visit date is before the configured max days threshold since they haven't actually logged in yet! We need to
* catch that case and update their last visit day to today to prevent blocking them all over again.
*
* However, Joomla! only allows us to see data onUserBeforeSave, not update them. Therefore I am using the
* onUserBeforeSave event to find out which user accounts are being saved and which need fiddling with per above.
* Then I used onUserAfterSave to update their lastVisitDate.
*
* @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)
{
// I only care about editing users from the backend
if ($this->container->platform->isFrontend())
{
return;
}
// I only care about editing existing users
if ($isNew)
{
return;
}
// I don't care if you are editing yourself
if ($oldUser['id'] == $this->container->platform->getUser()->id)
{
return;
}
// Do not process the user I am already updating after save.
if (in_array($oldUser['id'], $this->toUpdateUsers))
{
return;
}
// Do not process the user I have already updated after save.
if (in_array($oldUser['id'], $this->updatedUsers))
{
return;
}
$action = $this->cparams->getValue($this->settingsKey . '_action', 'reset') == 'block' ? 'block' : 'reset';
switch ($action)
{
case 'block':
// If the user wasn't blocked I have nothing to do
if ($oldUser['block'] == 0)
{
return;
}
// If you didn't change the user block status I have nothing to do
if ($data['block'] == 1)
{
return;
}
break;
case 'reset':
default:
// If the user wasn't required to password reset I have nothing to do
if ($oldUser['requireReset'] == 0)
{
return;
}
// If you didn't change the user's required password reset status I have nothing to do
if ($data['requireReset'] == 1)
{
return;
}
break;
}
// You are possibly editing a user I previously disabled automatically. Is this REALLY the case?
if (!empty($oldUser['lastvisitDate']) && ($oldUser['lastvisitDate'] != $this->db->getNullDate()))
{
$now = Date::getInstance();
$lastLogin = Date::getInstance($oldUser['lastvisitDate']);
$diff = $now->diff($lastLogin, true);
// If the last login was within the allowed number of days you are editing a user I must NOT touch.
if ($diff->days <= $this->maxDays)
{
return;
}
}
// Mark this user as in need for post-save update
$this->toUpdateUsers[] = $oldUser['id'];
}
/**
* Part of the automatic update of manually unblocked users, as explained onUserBeforeSave.
*
* @param array $data The user data saved to the database
* @param bool $isNew Was that a new user?
* @param bool $result Did the save succeed?
* @param string $errorMessage The last error message while saving the user.
*
*
* @since 5.3.0
*/
public function onUserAfterSave($data, $isNew, $result, $errorMessage)
{
// I don't care about new users
if ($isNew)
{
return;
}
// I don't care about failed saves
if (!$result)
{
return;
}
// Get the user ID
$userID = $data['id'];
// Do not process the user I have already updated after save.
if (in_array($userID, $this->updatedUsers))
{
return;
}
// Do not process a user UNLESS I have marked them as in need for an update.
if (!in_array($userID, $this->toUpdateUsers))
{
return;
}
// Mark the user as having their last visit date updated
$this->updatedUsers[] = $userID;
// Update the last visit date to today
$user = $this->container->platform->getUser($userID);
$user->lastvisitDate = Date::getInstance()->toSql();
$user->save(true);
}
/**
* Find users who belong in the configured backend user groups and who have not logged in for at least the
* configured number of days. Then take the configured action against them (force password reset or block them).
*
* @throws Exception
* @since 5.3.0
*/
protected function disableObsoleteAdmins()
{
// Get applicable user groups
$groups = $this->getBackendUserGroups();
if (empty($groups))
{
return;
}
// Get all applicable users
$users = $this->getUsersByGroups($groups);
// No users? Nothing to do, then.
if (empty($users))
{
return;
}
// Remove "protected" users from this list
$users = array_unique($users);
$users = $this->removeProtectedUsers($users);
// No users left after this operation? Nothing to do, then.
if (empty($users))
{
return;
}
asort($users);
// Get the login date to trigger this feature
$now = Date::getInstance();
$interval = new DateInterval(sprintf('P%dD', $this->maxDays));
$then = $now->sub($interval)->toSql();
// Have any of these users not logged in for a while?
try
{
$db = $this->db;
$query = $db->getQuery(true)
->select([$db->qn('id')])
->from($db->qn('#__users'))
->where($db->qn('id') . ' IN (' . implode(', ', array_map([$db, 'q'], $users)) . ')')
->where($db->qn('block') . ' = ' . $db->q(0))
->where($db->qn('requireReset') . ' = ' . $db->q(0))
->where($db->qn('lastvisitDate') . ' <= ' . $db->q($then));
$actionUsers = $db->setQuery($query)->loadColumn(0);
}
catch (Exception $e)
{
// Database error. Bail out.
}
asort($actionUsers);
// Get the applicable action
$action = $this->cparams->getValue($this->settingsKey . '_action', 'reset') == 'block' ? 'block' : 'reset';
/**
* Am I trying to block all Super Users AND I am not protecting any Super Users THEN I will not block any Super
* Users at all.
*
* Why not do the same if I am forcing a password reset? Because in this case all Super Users can reset their
* password over email. No harm done. You don't get locked out of your site.
*
* Why do this even when I have protected users? Because the user may have chosen to protect non-Super-Users by
* accident / because they do not understand the consequences. In this case I have to make sure I am not
* blocking any Super Users on their site or they risk getting locked out of it permanently.
*/
if ($action == 'block')
{
$actionUsers = $this->filterActionableUsersToEnsureRemainingSuperUser($actionUsers);
}
// No users to take action against?.
if (empty($actionUsers))
{
return;
}
// Apply the action
$query = $db->getQuery(true)
->update($db->qn('#__users'))
->where($db->qn('id') . ' IN (' . implode(', ', array_map([$db, 'q'], $actionUsers)) . ')');
switch ($action)
{
case 'block':
$query->set($db->qn('block') . ' = ' . $db->q(1));
break;
case 'reset':
default:
$query->set($db->qn('requireReset') . ' = ' . $db->q(1));
break;
}
$db->setQuery($query)->execute();
}
/**
* Returns all Joomla! user groups
*
* @return array
*
* @since 5.3.0
*/
protected function getAllJoomlaUserGroups()
{
if (empty($this->allJoomlaUserGroups))
{
// Get all groups
$db = $this->db;
$query = $db->getQuery(true)
->select([$db->qn('id')])
->from($db->qn('#__usergroups'));
$this->allJoomlaUserGroups = $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->allJoomlaUserGroups))
{
$this->allJoomlaUserGroups = [];
}
}
return $this->allJoomlaUserGroups;
}
/**
* Get the user groups configured by the user, filtered by those which really have backend access. If no groups are
* configured we will use all groups with backend access.
*
* @since 5.3.0
*/
protected function getBackendUserGroups()
{
// Get the configured groups
$groups = $this->cparams->getValue($this->settingsKey . '_groups', []);
$groups = is_string($groups) ? explode(',', trim($groups)) : $groups;
$groups = array_filter($groups, function ($group) {
return (int) trim($group) != 0;
});
// No groups? Assume we're to look into all Joomla! user groups.
if (empty($groups))
{
$groups = $this->getAllJoomlaUserGroups();
// Filter the configured user groups by those with backend access
$groups = array_filter($groups, [$this, 'isBackendAccessGroup']);
}
return $groups;
}
/**
* Remove the protected users from the given $users list and return the remaining users
*
* @param array $users The users list to filter
*
* @return array The filtered list
*
* @since 5.3.0
*/
protected function removeProtectedUsers(array $users)
{
$protected = $this->getProtectedUsers();
if (empty($protected))
{
return $users;
}
return array_diff($users, $protected);
}
/**
* Filter the list of actionable users in a way that ensures at least one Super User will remain active on the site
*
* @param array $actionableUsers The list of actionable users to filter
*
* @return array The filtered list
*
* @since 5.3.0
*/
protected function filterActionableUsersToEnsureRemainingSuperUser($actionableUsers)
{
$protected = $this->getProtectedUsers();
$superUsers = $this->getSuperUsers();
// If I have any protected Super Users bail out; a Super User is guaranteed to exist on the site.
$protectedSuper = array_intersect($protected, $superUsers);
if (count($protectedSuper))
{
return $actionableUsers;
}
// Remove Super Users from list of blocked users
return array_diff($actionableUsers, $superUsers);
}
/**
* Return the user IDs of all active (non-blocked) Super Users on the site.
*
* @return array
*
* @since 5.3.0
*/
protected function getSuperUsers()
{
// Get the Super User groups
$groups = $this->getAllJoomlaUserGroups();
$superUserGroups = array_filter($groups, function ($group) {
return Access::checkGroup($group, 'core.admin', 1);
});
// Get all Super Users
$superUsers = $this->getUsersByGroups($superUserGroups);
$superUsers = array_unique($superUsers);
// Return only active (non-blocked) Super User account IDs
return array_filter($superUsers, function ($userID) {
return $this->container->platform->getUser($userID)->block == 0;
});
}
/**
* Get the protected users' IDs
*
* @return int[]
*
* @since 5.3.0
*/
protected function getProtectedUsers()
{
$protected = $this->cparams->getValue($this->settingsKey . '_protected', []);
$protected = is_string($protected) ? explode(',', trim($protected)) : $protected;
$protected = array_filter($protected, function ($userID) {
return (int) trim($userID) != 0;
});
return $protected;
}
/**
* Returns all user IDs belonging to any of the group IDs specified.
*
* @param array $groups List of all user group IDs we are interested in
*
* @return array
*
* @since 5.3.0
*/
protected function getUsersByGroups(array $groups)
{
$db = $this->db;
$query = $db->getQuery(true)
->select([$db->qn('user_id')])
->from($db->qn('#__user_usergroup_map'))
->where($db->qn('group_id') . ' IN(' . implode(',', array_map(function ($group) use ($db) {
return $db->q(trim($group));
}, $groups)) . ')');
$ret = $db->setQuery($query)->loadColumn(0);
if (empty($ret))
{
return [];
}
return $ret;
}
/**
* Return the frequency [minutes] for running this feature.
*
* @return int
*
* @since 5.3.0
*/
protected function getRunFrequency()
{
$minutes = (int) $this->cparams->getValue($this->settingsKey . '_freq', 60);
if ($minutes <= 0)
{
$minutes = 60;
}
return $minutes;
}
}