first commit
This commit is contained in:
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\Application\CMSApplication;
|
||||
use Joomla\CMS\Event\MultiFactor\Callback;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\Input\Input;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Multi-factor Authentication plugins' AJAX callback controller
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class CallbackController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @param array $config Plugin configuration
|
||||
* @param MVCFactoryInterface|null $factory MVC Factory for the com_users component
|
||||
* @param CMSApplication|null $app CMS application object
|
||||
* @param Input|null $input Joomla CMS input object
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null)
|
||||
{
|
||||
parent::__construct($config, $factory, $app, $input);
|
||||
|
||||
$this->registerDefaultTask('callback');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement a callback feature, typically used for OAuth2 authentication
|
||||
*
|
||||
* @param bool $cachable Can this view be cached
|
||||
* @param array|bool $urlparams An array of safe url parameters and their variable types.
|
||||
* @see \Joomla\CMS\Filter\InputFilter::clean() for valid values.
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function callback($cachable = false, $urlparams = false): void
|
||||
{
|
||||
$app = $this->app;
|
||||
|
||||
// Get the Method and make sure it's non-empty
|
||||
$method = $this->input->getCmd('method', '');
|
||||
|
||||
if (empty($method)) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
PluginHelper::importPlugin('multifactorauth');
|
||||
|
||||
$event = new Callback($method);
|
||||
$this->app->getDispatcher()->dispatch($event->getName(), $event);
|
||||
|
||||
/**
|
||||
* The first plugin to handle the request should either redirect or close the application. If we are still here
|
||||
* no plugin handled the request successfully. Show an error.
|
||||
*/
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\Application\CMSApplication;
|
||||
use Joomla\CMS\Date\Date;
|
||||
use Joomla\CMS\Event\MultiFactor\NotifyActionLog;
|
||||
use Joomla\CMS\Event\MultiFactor\Validate;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
use Joomla\CMS\User\UserFactoryAwareInterface;
|
||||
use Joomla\CMS\User\UserFactoryAwareTrait;
|
||||
use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
|
||||
use Joomla\Component\Users\Administrator\Model\CaptiveModel;
|
||||
use Joomla\Input\Input;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Captive Multi-factor Authentication page controller
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class CaptiveController extends BaseController implements UserFactoryAwareInterface
|
||||
{
|
||||
use UserFactoryAwareTrait;
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @param array $config Plugin configuration
|
||||
* @param MVCFactoryInterface|null $factory MVC Factory for the com_users component
|
||||
* @param CMSApplication|null $app CMS application object
|
||||
* @param Input|null $input Joomla CMS input object
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null)
|
||||
{
|
||||
parent::__construct($config, $factory, $app, $input);
|
||||
|
||||
$this->registerTask('captive', 'display');
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the captive login page
|
||||
*
|
||||
* @param boolean $cachable Ignored. This page is never cached.
|
||||
* @param boolean|array $urlparams Ignored. This page is never cached.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function display($cachable = false, $urlparams = false): void
|
||||
{
|
||||
$user = $this->app->getIdentity() ?: $this->getUserFactory()->loadUserById(0);
|
||||
|
||||
// Only allow logged in Users
|
||||
if ($user->guest) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
// Get the view object
|
||||
$viewLayout = $this->input->get('layout', 'default', 'string');
|
||||
$view = $this->getView(
|
||||
'Captive',
|
||||
'html',
|
||||
'',
|
||||
[
|
||||
'base_path' => $this->basePath,
|
||||
'layout' => $viewLayout,
|
||||
]
|
||||
);
|
||||
|
||||
$view->document = $this->app->getDocument();
|
||||
|
||||
// If we're already logged in go to the site's home page
|
||||
if ((int) $this->app->getSession()->get('com_users.mfa_checked', 0) === 1) {
|
||||
$url = Route::_('index.php?option=com_users&task=methods.display', false);
|
||||
|
||||
$this->setRedirect($url);
|
||||
}
|
||||
|
||||
// Pass the model to the view
|
||||
/** @var CaptiveModel $model */
|
||||
$model = $this->getModel('Captive');
|
||||
$view->setModel($model, true);
|
||||
|
||||
/** @var BackupcodesModel $codesModel */
|
||||
$codesModel = $this->getModel('Backupcodes');
|
||||
$view->setModel($codesModel, false);
|
||||
|
||||
try {
|
||||
// Suppress all modules on the page except those explicitly allowed
|
||||
$model->suppressAllModules();
|
||||
} catch (\Exception $e) {
|
||||
// If we can't kill the modules we can still survive.
|
||||
}
|
||||
|
||||
// Pass the MFA record ID to the model
|
||||
$recordId = $this->input->getInt('record_id', null);
|
||||
$model->setState('record_id', $recordId);
|
||||
|
||||
// Do not go through $this->display() because it overrides the model.
|
||||
$view->display();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the MFA code entered by the user
|
||||
*
|
||||
* @param bool $cachable Ignored. This page is never cached.
|
||||
* @param array $urlparameters Ignored. This page is never cached.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function validate($cachable = false, $urlparameters = [])
|
||||
{
|
||||
// CSRF Check
|
||||
$this->checkToken($this->input->getMethod());
|
||||
|
||||
// Get the MFA parameters from the request
|
||||
$recordId = $this->input->getInt('record_id', null);
|
||||
$code = $this->input->get('code', null, 'raw');
|
||||
/** @var CaptiveModel $model */
|
||||
$model = $this->getModel('Captive');
|
||||
|
||||
// Validate the MFA record
|
||||
$model->setState('record_id', $recordId);
|
||||
$record = $model->getRecord();
|
||||
|
||||
if (empty($record)) {
|
||||
$event = new NotifyActionLog('onComUsersCaptiveValidateInvalidMethod');
|
||||
$this->app->getDispatcher()->dispatch($event->getName(), $event);
|
||||
|
||||
throw new \RuntimeException(Text::_('COM_USERS_MFA_INVALID_METHOD'), 500);
|
||||
}
|
||||
|
||||
if (!$model->checkTryLimit($record)) {
|
||||
// The try limit is reached, show error and return
|
||||
$captiveURL = Route::_('index.php?option=com_users&view=captive&task=select', false);
|
||||
$message = Text::_('COM_USERS_MFA_TRY_LIMIT_REACHED');
|
||||
$this->setRedirect($captiveURL, $message, 'error');
|
||||
|
||||
$event = new NotifyActionLog('onComUsersCaptiveValidateTryLimitReached');
|
||||
$this->app->getDispatcher()->dispatch($event->getName(), $event);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the code
|
||||
$user = $this->app->getIdentity() ?: $this->getUserFactory()->loadUserById(0);
|
||||
|
||||
$event = new Validate($record, $user, $code);
|
||||
$results = $this->app
|
||||
->getDispatcher()
|
||||
->dispatch($event->getName(), $event)
|
||||
->getArgument('result', []);
|
||||
|
||||
$isValidCode = false;
|
||||
|
||||
if ($record->method === 'backupcodes') {
|
||||
/** @var BackupcodesModel $codesModel */
|
||||
$codesModel = $this->getModel('Backupcodes');
|
||||
$results = [$codesModel->isBackupCode($code, $user)];
|
||||
/**
|
||||
* This is required! Do not remove!
|
||||
*
|
||||
* There is a store() call below. It saves the in-memory MFA record to the database. That includes the
|
||||
* options key which contains the configuration of the Method. For backup codes, these are the actual codes
|
||||
* you can use. When we check for a backup code validity we also "burn" it, i.e. we remove it from the
|
||||
* options table and save that to the database. However, this DOES NOT update the $record here. Therefore
|
||||
* the call to saveRecord() would overwrite the database contents with a record that _includes_ the backup
|
||||
* code we had just burned. As a result the single use backup codes end up being multiple use.
|
||||
*
|
||||
* By doing a getRecord() here, right after we have "burned" any correct backup codes, we resolve this
|
||||
* issue. The loaded record will reflect the database contents where the options DO NOT include the code we
|
||||
* just used. Therefore the call to store() will result in the correct database state, i.e. the used backup
|
||||
* code being removed.
|
||||
*/
|
||||
$record = $model->getRecord();
|
||||
}
|
||||
|
||||
$isValidCode = array_reduce(
|
||||
$results,
|
||||
function (bool $carry, $result) {
|
||||
return $carry || \boolval($result);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
if (!$isValidCode) {
|
||||
// The code is wrong. Display an error and go back.
|
||||
$captiveURL = Route::_('index.php?option=com_users&view=captive&record_id=' . $recordId, false);
|
||||
$message = Text::_('COM_USERS_MFA_INVALID_CODE');
|
||||
$this->setRedirect($captiveURL, $message, 'error');
|
||||
|
||||
$event = new NotifyActionLog('onComUsersCaptiveValidateFailed', [$record->title]);
|
||||
$this->app->getDispatcher()->dispatch($event->getName(), $event);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the Last Used, UA and IP columns
|
||||
$jNow = Date::getInstance();
|
||||
|
||||
$record->last_used = $jNow->toSql();
|
||||
$record->tries = 0;
|
||||
$record->last_try = null;
|
||||
$record->store();
|
||||
|
||||
// Flag the user as fully logged in
|
||||
$session = $this->app->getSession();
|
||||
$session->set('com_users.mfa_checked', 1);
|
||||
$session->set('com_users.mandatory_mfa_setup', 0);
|
||||
|
||||
// Get the return URL stored by the plugin in the session
|
||||
$returnUrl = $session->get('com_users.return_url', '');
|
||||
|
||||
// If the return URL is not set or not internal to this site redirect to the site's front page
|
||||
if (empty($returnUrl) || !Uri::isInternal($returnUrl)) {
|
||||
$returnUrl = Uri::base();
|
||||
}
|
||||
|
||||
$this->setRedirect($returnUrl);
|
||||
|
||||
$event = new NotifyActionLog('onComUsersCaptiveValidateSuccess', [$record->title]);
|
||||
$this->app->getDispatcher()->dispatch($event->getName(), $event);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\Access\Exception\NotAllowed;
|
||||
use Joomla\CMS\Helper\ContentHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Users display controller.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class DisplayController extends BaseController
|
||||
{
|
||||
/**
|
||||
* The default view.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $default_view = 'users';
|
||||
|
||||
/**
|
||||
* Checks whether a user can see this view.
|
||||
*
|
||||
* @param string $view The view name.
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function canView($view)
|
||||
{
|
||||
$canDo = ContentHelper::getActions('com_users');
|
||||
|
||||
switch ($view) {
|
||||
// Special permissions.
|
||||
case 'groups':
|
||||
case 'group':
|
||||
case 'levels':
|
||||
case 'level':
|
||||
return $canDo->get('core.admin');
|
||||
|
||||
// Default permissions.
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to display a view.
|
||||
*
|
||||
* @param boolean $cachable If true, the view output will be cached
|
||||
* @param array $urlparams An array of safe URL parameters and their variable types.
|
||||
* @see \Joomla\CMS\Filter\InputFilter::clean() for valid values.
|
||||
*
|
||||
* @return BaseController|boolean This object to support chaining or false on failure.
|
||||
*
|
||||
* @since 1.5
|
||||
*/
|
||||
public function display($cachable = false, $urlparams = [])
|
||||
{
|
||||
$view = $this->input->get('view', 'users');
|
||||
$layout = $this->input->get('layout', 'default');
|
||||
$id = $this->input->getInt('id');
|
||||
|
||||
if (!$this->canView($view)) {
|
||||
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
// Check for edit form.
|
||||
if ($view === 'user' && $layout === 'edit' && !$this->checkEditId('com_users.edit.user', $id)) {
|
||||
// Somehow the person just went to the form - we don't allow that.
|
||||
if (!\count($this->app->getMessageQueue())) {
|
||||
$this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error');
|
||||
}
|
||||
|
||||
$this->setRedirect(Route::_('index.php?option=com_users&view=users', false));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($view === 'group' && $layout === 'edit' && !$this->checkEditId('com_users.edit.group', $id)) {
|
||||
// Somehow the person just went to the form - we don't allow that.
|
||||
if (!\count($this->app->getMessageQueue())) {
|
||||
$this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error');
|
||||
}
|
||||
|
||||
$this->setRedirect(Route::_('index.php?option=com_users&view=groups', false));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($view === 'level' && $layout === 'edit' && !$this->checkEditId('com_users.edit.level', $id)) {
|
||||
// Somehow the person just went to the form - we don't allow that.
|
||||
if (!\count($this->app->getMessageQueue())) {
|
||||
$this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error');
|
||||
}
|
||||
|
||||
$this->setRedirect(Route::_('index.php?option=com_users&view=levels', false));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($view === 'note' && $layout === 'edit' && !$this->checkEditId('com_users.edit.note', $id)) {
|
||||
// Somehow the person just went to the form - we don't allow that.
|
||||
if (!\count($this->app->getMessageQueue())) {
|
||||
$this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error');
|
||||
}
|
||||
|
||||
$this->setRedirect(Route::_('index.php?option=com_users&view=notes', false));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (\in_array($view, ['captive', 'callback', 'methods', 'method'])) {
|
||||
$controller = $this->factory->createController($view, 'Administrator', [], $this->app, $this->input);
|
||||
$task = $this->input->get('task', '');
|
||||
|
||||
return $controller->execute($task);
|
||||
}
|
||||
|
||||
return parent::display($cachable, $urlparams);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\Access\Access;
|
||||
use Joomla\CMS\MVC\Controller\FormController;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User view level controller class.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class GroupController extends FormController
|
||||
{
|
||||
/**
|
||||
* @var string The prefix to use with controller messages.
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $text_prefix = 'COM_USERS_GROUP';
|
||||
|
||||
/**
|
||||
* Method to check if you can save a new or existing record.
|
||||
*
|
||||
* Overrides Joomla\CMS\MVC\Controller\FormController::allowSave to check the core.admin permission.
|
||||
*
|
||||
* @param array $data An array of input data.
|
||||
* @param string $key The name of the key for the primary key.
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function allowSave($data, $key = 'id')
|
||||
{
|
||||
return ($this->app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit
|
||||
*
|
||||
* Checks that non-Super Admins are not editing Super Admins.
|
||||
*
|
||||
* @param array $data An array of input data.
|
||||
* @param string $key The name of the key for the primary key.
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function allowEdit($data = [], $key = 'id')
|
||||
{
|
||||
// Check if this group is a Super Admin
|
||||
if (Access::checkGroup($data[$key], 'core.admin')) {
|
||||
// If I'm not a Super Admin, then disallow the edit.
|
||||
if (!$this->app->getIdentity()->authorise('core.admin')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return parent::allowEdit($data, $key);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\Access\Exception\NotAllowed;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\AdminController;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User groups list controller class.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class GroupsController extends AdminController
|
||||
{
|
||||
/**
|
||||
* @var string The prefix to use with controller messages.
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $text_prefix = 'COM_USERS_GROUPS';
|
||||
|
||||
/**
|
||||
* Proxy for getModel.
|
||||
*
|
||||
* @param string $name The model name. Optional.
|
||||
* @param string $prefix The class prefix. Optional.
|
||||
* @param array $config Configuration array for model. Optional.
|
||||
*
|
||||
* @return object The model.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getModel($name = 'Group', $prefix = 'Administrator', $config = ['ignore_request' => true])
|
||||
{
|
||||
return parent::getModel($name, $prefix, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item.
|
||||
*
|
||||
* Overrides Joomla\CMS\MVC\Controller\AdminController::delete to check the core.admin permission.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) {
|
||||
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
parent::delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to publish a list of records.
|
||||
*
|
||||
* Overrides Joomla\CMS\MVC\Controller\AdminController::publish to check the core.admin permission.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function publish()
|
||||
{
|
||||
if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) {
|
||||
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
parent::publish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the order of one or more records.
|
||||
*
|
||||
* Overrides Joomla\CMS\MVC\Controller\AdminController::reorder to check the core.admin permission.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function reorder()
|
||||
{
|
||||
if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) {
|
||||
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
return parent::reorder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to save the submitted ordering values for records.
|
||||
*
|
||||
* Overrides Joomla\CMS\MVC\Controller\AdminController::saveorder to check the core.admin permission.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function saveorder()
|
||||
{
|
||||
if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) {
|
||||
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
return parent::saveorder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check in of one or more records.
|
||||
*
|
||||
* Overrides Joomla\CMS\MVC\Controller\AdminController::checkin to check the core.admin permission.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function checkin()
|
||||
{
|
||||
if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) {
|
||||
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
return parent::checkin();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\Access\Access;
|
||||
use Joomla\CMS\Access\Exception\NotAllowed;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\FormController;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User view level controller class.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class LevelController extends FormController
|
||||
{
|
||||
/**
|
||||
* @var string The prefix to use with controller messages.
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $text_prefix = 'COM_USERS_LEVEL';
|
||||
|
||||
/**
|
||||
* Method to check if you can save a new or existing record.
|
||||
*
|
||||
* Overrides Joomla\CMS\MVC\Controller\FormController::allowSave to check the core.admin permission.
|
||||
*
|
||||
* @param array $data An array of input data.
|
||||
* @param string $key The name of the key for the primary key.
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function allowSave($data, $key = 'id')
|
||||
{
|
||||
return ($this->app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides JControllerForm::allowEdit
|
||||
*
|
||||
* Checks that non-Super Admins are not editing Super Admins.
|
||||
*
|
||||
* @param array $data An array of input data.
|
||||
* @param string $key The name of the key for the primary key.
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @since 3.8.8
|
||||
*/
|
||||
protected function allowEdit($data = [], $key = 'id')
|
||||
{
|
||||
// Check for if Super Admin can edit
|
||||
$viewLevel = $this->getModel('Level', 'Administrator')->getItem((int) $data['id']);
|
||||
|
||||
// If this group is super admin and this user is not super admin, canEdit is false
|
||||
if (!$this->app->getIdentity()->authorise('core.admin') && $viewLevel->rules && Access::checkGroup($viewLevel->rules[0], 'core.admin')) {
|
||||
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error');
|
||||
|
||||
$this->setRedirect(
|
||||
Route::_(
|
||||
'index.php?option=' . $this->option . '&view=' . $this->view_list
|
||||
. $this->getRedirectToListAppend(),
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::allowEdit($data, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item.
|
||||
*
|
||||
* Overrides Joomla\CMS\MVC\Controller\FormController::delete to check the core.admin permission.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
// Check for request forgeries.
|
||||
$this->checkToken();
|
||||
|
||||
$ids = (array) $this->input->get('cid', [], 'int');
|
||||
|
||||
// Remove zero values resulting from input filter
|
||||
$ids = array_filter($ids);
|
||||
|
||||
if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) {
|
||||
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
if (empty($ids)) {
|
||||
$this->setMessage(Text::_('COM_USERS_NO_LEVELS_SELECTED'), 'warning');
|
||||
} else {
|
||||
// Get the model.
|
||||
$model = $this->getModel();
|
||||
|
||||
// Remove the items.
|
||||
if ($model->delete($ids)) {
|
||||
$this->setMessage(Text::plural('COM_USERS_N_LEVELS_DELETED', \count($ids)));
|
||||
}
|
||||
}
|
||||
|
||||
$this->setRedirect('index.php?option=com_users&view=levels');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\MVC\Controller\AdminController;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User view levels list controller class.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class LevelsController extends AdminController
|
||||
{
|
||||
/**
|
||||
* @var string The prefix to use with controller messages.
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $text_prefix = 'COM_USERS_LEVELS';
|
||||
|
||||
/**
|
||||
* Proxy for getModel.
|
||||
*
|
||||
* @param string $name The model name. Optional.
|
||||
* @param string $prefix The class prefix. Optional.
|
||||
* @param array $config Configuration array for model. Optional.
|
||||
*
|
||||
* @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getModel($name = 'Level', $prefix = 'Administrator', $config = ['ignore_request' => true])
|
||||
{
|
||||
return parent::getModel($name, $prefix, $config);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Users mail controller.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class MailController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Send the mail
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function send()
|
||||
{
|
||||
// Redirect to admin index if mass mailer disabled in conf
|
||||
if ($this->app->get('massmailoff', 0) == 1) {
|
||||
$this->app->redirect(Route::_('index.php', false));
|
||||
}
|
||||
|
||||
// Check for request forgeries.
|
||||
$this->checkToken('request');
|
||||
|
||||
$model = $this->getModel('Mail');
|
||||
|
||||
if ($model->send()) {
|
||||
$type = 'message';
|
||||
} else {
|
||||
$type = 'error';
|
||||
}
|
||||
|
||||
$msg = $model->getError();
|
||||
$this->setRedirect('index.php?option=com_users&view=mail', $msg, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the mail
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function cancel()
|
||||
{
|
||||
// Check for request forgeries.
|
||||
$this->checkToken('request');
|
||||
|
||||
// Clear data from session.
|
||||
$this->app->setUserState('com_users.display.mail.data', null);
|
||||
|
||||
$this->setRedirect('index.php?option=com_users&view=users');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,486 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\Application\CMSApplication;
|
||||
use Joomla\CMS\Event\MultiFactor\NotifyActionLog;
|
||||
use Joomla\CMS\Event\MultiFactor\SaveSetup;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\BaseController as BaseControllerAlias;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
use Joomla\CMS\User\User;
|
||||
use Joomla\CMS\User\UserFactoryAwareInterface;
|
||||
use Joomla\CMS\User\UserFactoryAwareTrait;
|
||||
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
|
||||
use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
|
||||
use Joomla\Component\Users\Administrator\Model\MethodModel;
|
||||
use Joomla\Component\Users\Administrator\Table\MfaTable;
|
||||
use Joomla\Input\Input;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Multi-factor Authentication method controller
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class MethodController extends BaseControllerAlias implements UserFactoryAwareInterface
|
||||
{
|
||||
use UserFactoryAwareTrait;
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @param array $config Plugin configuration
|
||||
* @param MVCFactoryInterface|null $factory MVC Factory for the com_users component
|
||||
* @param CMSApplication|null $app CMS application object
|
||||
* @param Input|null $input Joomla CMS input object
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null)
|
||||
{
|
||||
// We have to tell Joomla what is the name of the view, otherwise it defaults to the name of the *component*.
|
||||
$config['default_view'] = 'method';
|
||||
$config['default_task'] = 'add';
|
||||
|
||||
parent::__construct($config, $factory, $app, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a task by triggering a Method in the derived class.
|
||||
*
|
||||
* @param string $task The task to perform. If no matching task is found, the '__default' task is executed, if
|
||||
* defined.
|
||||
*
|
||||
* @return mixed The value returned by the called Method.
|
||||
*
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function execute($task)
|
||||
{
|
||||
if (empty($task) || $task === 'display') {
|
||||
$task = 'add';
|
||||
}
|
||||
|
||||
return parent::execute($task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new MFA Method
|
||||
*
|
||||
* @param boolean $cachable Ignored. This page is never cached.
|
||||
* @param boolean|array $urlparams Ignored. This page is never cached.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function add($cachable = false, $urlparams = []): void
|
||||
{
|
||||
$this->assertLoggedInUser();
|
||||
|
||||
// Make sure I am allowed to edit the specified user
|
||||
$userId = $this->input->getInt('user_id', null);
|
||||
$user = $this->getUserFactory()->loadUserById($userId);
|
||||
|
||||
$this->assertCanEdit($user);
|
||||
|
||||
// Also make sure the Method really does exist
|
||||
$method = $this->input->getCmd('method');
|
||||
$this->assertMethodExists($method);
|
||||
|
||||
/** @var MethodModel $model */
|
||||
$model = $this->getModel('Method');
|
||||
$model->setState('method', $method);
|
||||
|
||||
// Pass the return URL to the view
|
||||
$returnURL = $this->input->getBase64('returnurl');
|
||||
$viewLayout = $this->input->get('layout', 'default', 'string');
|
||||
$view = $this->getView('Method', 'html');
|
||||
$view->setLayout($viewLayout);
|
||||
$view->returnURL = $returnURL;
|
||||
$view->user = $user;
|
||||
$view->document = $this->app->getDocument();
|
||||
|
||||
$view->setModel($model, true);
|
||||
|
||||
$event = new NotifyActionLog('onComUsersControllerMethodBeforeAdd', [$user, $method]);
|
||||
$this->app->getDispatcher()->dispatch($event->getName(), $event);
|
||||
|
||||
$view->display();
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit an existing MFA Method
|
||||
*
|
||||
* @param boolean $cachable Ignored. This page is never cached.
|
||||
* @param boolean|array $urlparams Ignored. This page is never cached.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function edit($cachable = false, $urlparams = []): void
|
||||
{
|
||||
$this->assertLoggedInUser();
|
||||
|
||||
// Make sure I am allowed to edit the specified user
|
||||
$userId = $this->input->getInt('user_id', null);
|
||||
$user = $this->getUserFactory()->loadUserById($userId);
|
||||
|
||||
$this->assertCanEdit($user);
|
||||
|
||||
// Also make sure the Method really does exist
|
||||
$id = $this->input->getInt('id');
|
||||
$record = $this->assertValidRecordId($id, $user);
|
||||
|
||||
if ($id <= 0) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
/** @var MethodModel $model */
|
||||
$model = $this->getModel('Method');
|
||||
$model->setState('id', $id);
|
||||
|
||||
// Pass the return URL to the view
|
||||
$returnURL = $this->input->getBase64('returnurl');
|
||||
$viewLayout = $this->input->get('layout', 'default', 'string');
|
||||
$view = $this->getView('Method', 'html');
|
||||
$view->setLayout($viewLayout);
|
||||
$view->returnURL = $returnURL;
|
||||
$view->user = $user;
|
||||
$view->document = $this->app->getDocument();
|
||||
|
||||
$view->setModel($model, true);
|
||||
|
||||
$event = new NotifyActionLog('onComUsersControllerMethodBeforeEdit', [$id, $user]);
|
||||
$this->app->getDispatcher()->dispatch($event->getName(), $event);
|
||||
|
||||
$view->display();
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate backup codes
|
||||
*
|
||||
* @param boolean $cachable Ignored. This page is never cached.
|
||||
* @param boolean|array $urlparams Ignored. This page is never cached.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function regenerateBackupCodes($cachable = false, $urlparams = []): void
|
||||
{
|
||||
$this->assertLoggedInUser();
|
||||
|
||||
$this->checkToken($this->input->getMethod());
|
||||
|
||||
// Make sure I am allowed to edit the specified user
|
||||
$userId = $this->input->getInt('user_id', null);
|
||||
$user = $this->getUserFactory()->loadUserById($userId);
|
||||
$this->assertCanEdit($user);
|
||||
|
||||
/** @var BackupcodesModel $model */
|
||||
$model = $this->getModel('Backupcodes');
|
||||
$model->regenerateBackupCodes($user);
|
||||
|
||||
$backupCodesRecord = $model->getBackupCodesRecord($user);
|
||||
|
||||
// Redirect
|
||||
$redirectUrl = 'index.php?option=com_users&task=method.edit&user_id=' . $userId . '&id=' . $backupCodesRecord->id;
|
||||
$returnURL = $this->input->getBase64('returnurl');
|
||||
|
||||
if (!empty($returnURL) && Uri::isInternal(base64_decode($returnURL))) {
|
||||
$redirectUrl .= '&returnurl=' . $returnURL;
|
||||
}
|
||||
|
||||
$this->setRedirect(Route::_($redirectUrl, false));
|
||||
|
||||
$event = new NotifyActionLog('onComUsersControllerMethodAfterRegenerateBackupCodes');
|
||||
$this->app->getDispatcher()->dispatch($event->getName(), $event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing MFA Method
|
||||
*
|
||||
* @param boolean $cachable Ignored. This page is never cached.
|
||||
* @param boolean|array $urlparams Ignored. This page is never cached.
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function delete($cachable = false, $urlparams = []): void
|
||||
{
|
||||
$this->assertLoggedInUser();
|
||||
|
||||
$this->checkToken($this->input->getMethod());
|
||||
|
||||
// Make sure I am allowed to edit the specified user
|
||||
$userId = $this->input->getInt('user_id', null);
|
||||
$user = $this->getUserFactory()->loadUserById($userId);
|
||||
$this->assertCanDelete($user);
|
||||
|
||||
// Also make sure the Method really does exist
|
||||
$id = $this->input->getInt('id');
|
||||
$record = $this->assertValidRecordId($id, $user);
|
||||
|
||||
if ($id <= 0) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
$type = null;
|
||||
$message = null;
|
||||
|
||||
$event = new NotifyActionLog('onComUsersControllerMethodBeforeDelete', [$id, $user]);
|
||||
$this->app->getDispatcher()->dispatch($event->getName(), $event);
|
||||
|
||||
try {
|
||||
$record->delete();
|
||||
} catch (\Exception $e) {
|
||||
$message = $e->getMessage();
|
||||
$type = 'error';
|
||||
}
|
||||
|
||||
// Redirect
|
||||
$url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false);
|
||||
$returnURL = $this->input->getBase64('returnurl');
|
||||
|
||||
if (!empty($returnURL) && Uri::isInternal(base64_decode($returnURL))) {
|
||||
$url = base64_decode($returnURL);
|
||||
}
|
||||
|
||||
$this->setRedirect($url, $message, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the MFA Method
|
||||
*
|
||||
* @param boolean $cachable Ignored. This page is never cached.
|
||||
* @param boolean|array $urlparams Ignored. This page is never cached.
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function save($cachable = false, $urlparams = []): void
|
||||
{
|
||||
$this->assertLoggedInUser();
|
||||
|
||||
$this->checkToken($this->input->getMethod());
|
||||
|
||||
// Make sure I am allowed to edit the specified user
|
||||
$userId = $this->input->getInt('user_id', null);
|
||||
$user = $this->getUserFactory()->loadUserById($userId);
|
||||
$this->assertCanEdit($user);
|
||||
|
||||
// Redirect
|
||||
$url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false);
|
||||
$returnURL = $this->input->getBase64('returnurl');
|
||||
|
||||
if (!empty($returnURL) && Uri::isInternal(base64_decode($returnURL))) {
|
||||
$url = base64_decode($returnURL);
|
||||
}
|
||||
|
||||
// The record must either be new (ID zero) or exist
|
||||
$id = $this->input->getInt('id', 0);
|
||||
$record = $this->assertValidRecordId($id, $user);
|
||||
|
||||
// If it's a new record we need to read the Method from the request and update the (not yet created) record.
|
||||
if ($record->id == 0) {
|
||||
$methodName = $this->input->getCmd('method');
|
||||
$this->assertMethodExists($methodName);
|
||||
$record->method = $methodName;
|
||||
}
|
||||
|
||||
/** @var MethodModel $model */
|
||||
$model = $this->getModel('Method');
|
||||
|
||||
// Ask the plugin to validate the input by calling onUserMultifactorSaveSetup
|
||||
$result = [];
|
||||
$input = $this->app->getInput();
|
||||
|
||||
$event = new NotifyActionLog('onComUsersControllerMethodBeforeSave', [$id, $user]);
|
||||
$this->app->getDispatcher()->dispatch($event->getName(), $event);
|
||||
|
||||
try {
|
||||
$event = new SaveSetup($record, $input);
|
||||
$pluginResults = $this->app
|
||||
->getDispatcher()
|
||||
->dispatch($event->getName(), $event)
|
||||
->getArgument('result', []);
|
||||
|
||||
foreach ($pluginResults as $pluginResult) {
|
||||
$result = array_merge($result, $pluginResult);
|
||||
}
|
||||
} catch (\RuntimeException $e) {
|
||||
// Go back to the edit page
|
||||
$nonSefUrl = 'index.php?option=com_users&task=method.';
|
||||
|
||||
if ($id) {
|
||||
$nonSefUrl .= 'edit&id=' . (int) $id;
|
||||
} else {
|
||||
$nonSefUrl .= 'add&method=' . $record->method;
|
||||
}
|
||||
|
||||
$nonSefUrl .= '&user_id=' . $userId;
|
||||
|
||||
if (!empty($returnURL)) {
|
||||
$nonSefUrl .= '&returnurl=' . urlencode($returnURL);
|
||||
}
|
||||
|
||||
$url = Route::_($nonSefUrl, false);
|
||||
$this->setRedirect($url, $e->getMessage(), 'error');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the record's options with the plugin response
|
||||
$title = $this->input->getString('title', null);
|
||||
$title = trim($title);
|
||||
|
||||
if (empty($title)) {
|
||||
$method = $model->getMethod($record->method);
|
||||
$title = $method['display'];
|
||||
}
|
||||
|
||||
// Update the record's "default" flag
|
||||
$default = $this->input->getBool('default', false);
|
||||
$record->title = $title;
|
||||
$record->options = $result;
|
||||
$record->default = $default ? 1 : 0;
|
||||
|
||||
// Ask the model to save the record
|
||||
$saved = $record->store();
|
||||
|
||||
if (!$saved) {
|
||||
// Go back to the edit page
|
||||
$nonSefUrl = 'index.php?option=com_users&task=method.';
|
||||
|
||||
if ($id) {
|
||||
$nonSefUrl .= 'edit&id=' . (int) $id;
|
||||
} else {
|
||||
$nonSefUrl .= 'add';
|
||||
}
|
||||
|
||||
$nonSefUrl .= '&user_id=' . $userId;
|
||||
|
||||
if (!empty($returnURL)) {
|
||||
$nonSefUrl .= '&returnurl=' . urlencode($returnURL);
|
||||
}
|
||||
|
||||
$url = Route::_($nonSefUrl, false);
|
||||
$this->setRedirect($url, $record->getError(), 'error');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setRedirect($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the provided ID is a valid record identified for the given user
|
||||
*
|
||||
* @param int $id Record ID to check
|
||||
* @param User|null $user User record. Null to use current user.
|
||||
*
|
||||
* @return MfaTable The loaded record
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function assertValidRecordId($id, ?User $user = null): MfaTable
|
||||
{
|
||||
if (\is_null($user)) {
|
||||
$user = $this->app->getIdentity() ?: $this->getUserFactory()->loadUserById(0);
|
||||
}
|
||||
|
||||
/** @var MethodModel $model */
|
||||
$model = $this->getModel('Method');
|
||||
|
||||
$model->setState('id', $id);
|
||||
|
||||
$record = $model->getRecord($user);
|
||||
|
||||
if (\is_null($record) || ($record->id != $id) || ($record->user_id != $user->id)) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the user can add / edit MFA methods.
|
||||
*
|
||||
* @param User|null $user User record. Null to use current user.
|
||||
*
|
||||
* @return void
|
||||
* @throws \RuntimeException|\Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function assertCanEdit(?User $user = null): void
|
||||
{
|
||||
if (!MfaHelper::canAddEditMethod($user)) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the user can delete MFA records / disable MFA.
|
||||
*
|
||||
* @param User|null $user User record. Null to use current user.
|
||||
*
|
||||
* @return void
|
||||
* @throws \RuntimeException|\Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function assertCanDelete(?User $user = null): void
|
||||
{
|
||||
if (!MfaHelper::canDeleteMethod($user)) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the specified MFA Method exists, is activated and enabled for the current user
|
||||
*
|
||||
* @param string|null $method The Method to check
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function assertMethodExists(?string $method): void
|
||||
{
|
||||
/** @var MethodModel $model */
|
||||
$model = $this->getModel('Method');
|
||||
|
||||
if (empty($method) || !$model->methodExists($method)) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that there is a logged in user.
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function assertLoggedInUser(): void
|
||||
{
|
||||
$user = $this->app->getIdentity() ?: $this->getUserFactory()->loadUserById(0);
|
||||
|
||||
if ($user->guest) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\Application\CMSApplication;
|
||||
use Joomla\CMS\Event\MultiFactor\NotifyActionLog;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
use Joomla\CMS\User\UserFactoryAwareInterface;
|
||||
use Joomla\CMS\User\UserFactoryAwareTrait;
|
||||
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
|
||||
use Joomla\Component\Users\Administrator\Model\MethodsModel;
|
||||
use Joomla\Input\Input;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Multi-factor Authentication methods selection and management controller
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class MethodsController extends BaseController implements UserFactoryAwareInterface
|
||||
{
|
||||
use UserFactoryAwareTrait;
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @param array $config Plugin configuration
|
||||
* @param MVCFactoryInterface|null $factory MVC Factory for the com_users component
|
||||
* @param CMSApplication|null $app CMS application object
|
||||
* @param Input|null $input Joomla CMS input object
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function __construct($config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null)
|
||||
{
|
||||
// We have to tell Joomla what is the name of the view, otherwise it defaults to the name of the *component*.
|
||||
$config['default_view'] = 'Methods';
|
||||
|
||||
parent::__construct($config, $factory, $app, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable Multi-factor Authentication for the current user
|
||||
*
|
||||
* @param bool $cachable Can this view be cached
|
||||
* @param array $urlparams An array of safe url parameters and their variable types.
|
||||
* @see \Joomla\CMS\Filter\InputFilter::clean() for valid values.
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function disable($cachable = false, $urlparams = []): void
|
||||
{
|
||||
$this->assertLoggedInUser();
|
||||
|
||||
$this->checkToken($this->input->getMethod());
|
||||
|
||||
// Make sure I am allowed to edit the specified user
|
||||
$userId = $this->input->getInt('user_id', null);
|
||||
$user = ($userId === null)
|
||||
? $this->app->getIdentity()
|
||||
: $this->getUserFactory()->loadUserById($userId);
|
||||
$user = $user ?? $this->getUserFactory()->loadUserById(0);
|
||||
|
||||
if (!MfaHelper::canDeleteMethod($user)) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
// Delete all MFA Methods for the user
|
||||
/** @var MethodsModel $model */
|
||||
$model = $this->getModel('Methods');
|
||||
$type = null;
|
||||
$message = null;
|
||||
|
||||
$event = new NotifyActionLog('onComUsersControllerMethodsBeforeDisable', [$user]);
|
||||
$this->app->getDispatcher()->dispatch($event->getName(), $event);
|
||||
|
||||
try {
|
||||
$model->deleteAll($user);
|
||||
} catch (\Exception $e) {
|
||||
$message = $e->getMessage();
|
||||
$type = 'error';
|
||||
}
|
||||
|
||||
// Redirect
|
||||
$url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false);
|
||||
$returnURL = $this->input->getBase64('returnurl');
|
||||
|
||||
if (!empty($returnURL) && Uri::isInternal(base64_decode($returnURL))) {
|
||||
$url = base64_decode($returnURL);
|
||||
}
|
||||
|
||||
$this->setRedirect($url, $message, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available Multi-factor Authentication Methods available and guide the user to setting them up
|
||||
*
|
||||
* @param bool $cachable Can this view be cached
|
||||
* @param array $urlparams An array of safe url parameters and their variable types.
|
||||
* @see \Joomla\CMS\Filter\InputFilter::clean() for valid values.
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function display($cachable = false, $urlparams = []): void
|
||||
{
|
||||
$this->assertLoggedInUser();
|
||||
|
||||
// Make sure I am allowed to edit the specified user
|
||||
$userId = $this->input->getInt('user_id', null);
|
||||
$user = ($userId === null)
|
||||
? $this->app->getIdentity()
|
||||
: $this->getUserFactory()->loadUserById($userId);
|
||||
$user = $user ?? $this->getUserFactory()->loadUserById(0);
|
||||
|
||||
if (!MfaHelper::canShowConfigurationInterface($user)) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
$returnURL = $this->input->getBase64('returnurl');
|
||||
$viewLayout = $this->input->get('layout', 'default', 'string');
|
||||
$view = $this->getView('Methods', 'html');
|
||||
$view->setLayout($viewLayout);
|
||||
$view->returnURL = $returnURL;
|
||||
$view->user = $user;
|
||||
$view->document = $this->app->getDocument();
|
||||
|
||||
$methodsModel = $this->getModel('Methods');
|
||||
$view->setModel($methodsModel, true);
|
||||
|
||||
$backupCodesModel = $this->getModel('Backupcodes');
|
||||
$view->setModel($backupCodesModel, false);
|
||||
|
||||
$view->display();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable Multi-factor Authentication for the current user
|
||||
*
|
||||
* @param bool $cachable Can this view be cached
|
||||
* @param array $urlparams An array of safe url parameters and their variable types.
|
||||
* @see \Joomla\CMS\Filter\InputFilter::clean() for valid values.
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function doNotShowThisAgain($cachable = false, $urlparams = []): void
|
||||
{
|
||||
$this->assertLoggedInUser();
|
||||
|
||||
$this->checkToken($this->input->getMethod());
|
||||
|
||||
// Make sure I am allowed to edit the specified user
|
||||
$userId = $this->input->getInt('user_id', null);
|
||||
$user = ($userId === null)
|
||||
? $this->app->getIdentity()
|
||||
: $this->getUserFactory()->loadUserById($userId);
|
||||
$user = $user ?? $this->getUserFactory()->loadUserById(0);
|
||||
|
||||
if (!MfaHelper::canAddEditMethod($user)) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
$event = new NotifyActionLog('onComUsersControllerMethodsBeforeDoNotShowThisAgain', [$user]);
|
||||
$this->app->getDispatcher()->dispatch($event->getName(), $event);
|
||||
|
||||
/** @var MethodsModel $model */
|
||||
$model = $this->getModel('Methods');
|
||||
$model->setFlag($user, true);
|
||||
|
||||
// Redirect
|
||||
$url = Uri::base();
|
||||
$returnURL = $this->input->getBase64('returnurl');
|
||||
|
||||
if (!empty($returnURL) && Uri::isInternal(base64_decode($returnURL))) {
|
||||
$url = base64_decode($returnURL);
|
||||
}
|
||||
|
||||
$this->setRedirect($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that there is a user currently logged in
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function assertLoggedInUser(): void
|
||||
{
|
||||
$user = $this->app->getIdentity() ?: $this->getUserFactory()->loadUserById(0);
|
||||
|
||||
if ($user->guest) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\MVC\Controller\FormController;
|
||||
use Joomla\CMS\Versioning\VersionableControllerTrait;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User note controller class.
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
class NoteController extends FormController
|
||||
{
|
||||
use VersionableControllerTrait;
|
||||
|
||||
/**
|
||||
* The prefix to use with controller messages.
|
||||
*
|
||||
* @var string
|
||||
* @since 2.5
|
||||
*/
|
||||
protected $text_prefix = 'COM_USERS_NOTE';
|
||||
|
||||
/**
|
||||
* Gets the URL arguments to append to an item redirect.
|
||||
*
|
||||
* @param integer $recordId The primary key id for the item.
|
||||
* @param string $key The name of the primary key variable.
|
||||
*
|
||||
* @return string The arguments to append to the redirect URL.
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
protected function getRedirectToItemAppend($recordId = null, $key = 'id')
|
||||
{
|
||||
$append = parent::getRedirectToItemAppend($recordId, $key);
|
||||
|
||||
$userId = $this->input->get('u_id', 0, 'int');
|
||||
|
||||
if ($userId) {
|
||||
$append .= '&u_id=' . $userId;
|
||||
}
|
||||
|
||||
return $append;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\MVC\Controller\AdminController;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User notes controller class.
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
class NotesController extends AdminController
|
||||
{
|
||||
/**
|
||||
* The prefix to use with controller messages.
|
||||
*
|
||||
* @var string
|
||||
* @since 2.5
|
||||
*/
|
||||
protected $text_prefix = 'COM_USERS_NOTES';
|
||||
|
||||
/**
|
||||
* Method to get a model object, loading it if required.
|
||||
*
|
||||
* @param string $name The model name. Optional.
|
||||
* @param string $prefix The class prefix. Optional.
|
||||
* @param array $config Configuration array for model. Optional.
|
||||
*
|
||||
* @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model.
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
public function getModel($name = 'Note', $prefix = 'Administrator', $config = ['ignore_request' => true])
|
||||
{
|
||||
return parent::getModel($name, $prefix, $config);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2007 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\Access\Access;
|
||||
use Joomla\CMS\MVC\Controller\FormController;
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User controller class.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class UserController extends FormController
|
||||
{
|
||||
/**
|
||||
* @var string The prefix to use with controller messages.
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $text_prefix = 'COM_USERS_USER';
|
||||
|
||||
/**
|
||||
* Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit
|
||||
*
|
||||
* Checks that non-Super Admins are not editing Super Admins.
|
||||
*
|
||||
* @param array $data An array of input data.
|
||||
* @param string $key The name of the key for the primary key.
|
||||
*
|
||||
* @return boolean True if allowed, false otherwise.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function allowEdit($data = [], $key = 'id')
|
||||
{
|
||||
// Check if this person is a Super Admin
|
||||
if (Access::check($data[$key], 'core.admin')) {
|
||||
// If I'm not a Super Admin, then disallow the edit.
|
||||
if (!$this->app->getIdentity()->authorise('core.admin')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow users to edit their own account
|
||||
if (isset($data[$key]) && (int) $this->app->getIdentity()->id === (int) $data[$key]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::allowEdit($data, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override parent cancel to redirect when using status edit account.
|
||||
*
|
||||
* @param string $key The name of the primary key of the URL variable.
|
||||
*
|
||||
* @return boolean True if access level checks pass, false otherwise.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function cancel($key = null)
|
||||
{
|
||||
$result = parent::cancel();
|
||||
|
||||
if ($return = $this->input->get('return', '', 'BASE64')) {
|
||||
$return = base64_decode($return);
|
||||
|
||||
// Don't redirect to an external URL.
|
||||
if (!Uri::isInternal($return)) {
|
||||
$return = Uri::base();
|
||||
}
|
||||
|
||||
$this->app->redirect($return);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override parent save to redirect when using status edit account.
|
||||
*
|
||||
* @param string $key The name of the primary key of the URL variable.
|
||||
* @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions).
|
||||
*
|
||||
* @return boolean True if successful, false otherwise.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function save($key = null, $urlVar = null)
|
||||
{
|
||||
$result = parent::save($key, $urlVar);
|
||||
|
||||
$task = $this->getTask();
|
||||
|
||||
if ($task === 'save' && $return = $this->input->get('return', '', 'BASE64')) {
|
||||
$return = base64_decode($return);
|
||||
|
||||
// Don't redirect to an external URL.
|
||||
if (!Uri::isInternal($return)) {
|
||||
$return = Uri::base();
|
||||
}
|
||||
|
||||
$this->setRedirect($return);
|
||||
}
|
||||
|
||||
// If a user has to renew a password but has no permission for users
|
||||
if (!$this->app->getIdentity()->authorise('core.admin', 'com_users')) {
|
||||
$this->setRedirect('index.php');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to run batch operations.
|
||||
*
|
||||
* @param object $model The model.
|
||||
*
|
||||
* @return boolean True on success, false on failure
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
public function batch($model = null)
|
||||
{
|
||||
$this->checkToken();
|
||||
|
||||
// Set the model
|
||||
$model = $this->getModel('User', 'Administrator', []);
|
||||
|
||||
// Preset the redirect
|
||||
$this->setRedirect(Route::_('index.php?option=com_users&view=users' . $this->getRedirectToListAppend(), false));
|
||||
|
||||
return parent::batch($model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that allows child controller access to model data after the data has been saved.
|
||||
*
|
||||
* @param BaseDatabaseModel $model The data model object.
|
||||
* @param array $validData The validated data.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 3.1
|
||||
*/
|
||||
protected function postSaveHook(BaseDatabaseModel $model, $validData = [])
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Controller;
|
||||
|
||||
use Joomla\CMS\Application\CMSApplication;
|
||||
use Joomla\CMS\Input\Input;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\AdminController;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\Response\JsonResponse;
|
||||
use Joomla\Utilities\ArrayHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Users list controller class.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class UsersController extends AdminController
|
||||
{
|
||||
/**
|
||||
* @var string The prefix to use with controller messages.
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $text_prefix = 'COM_USERS_USERS';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $config An optional associative array of configuration settings.
|
||||
* @param MVCFactoryInterface $factory The factory.
|
||||
* @param CMSApplication $app The CMSApplication for the dispatcher
|
||||
* @param Input $input Input
|
||||
*
|
||||
* @since 1.6
|
||||
* @see BaseController
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct($config = [], MVCFactoryInterface $factory = null, $app = null, $input = null)
|
||||
{
|
||||
parent::__construct($config, $factory, $app, $input);
|
||||
|
||||
$this->registerTask('block', 'changeBlock');
|
||||
$this->registerTask('unblock', 'changeBlock');
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy for getModel.
|
||||
*
|
||||
* @param string $name The model name. Optional.
|
||||
* @param string $prefix The class prefix. Optional.
|
||||
* @param array $config Configuration array for model. Optional.
|
||||
*
|
||||
* @return object The model.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getModel($name = 'User', $prefix = 'Administrator', $config = ['ignore_request' => true])
|
||||
{
|
||||
return parent::getModel($name, $prefix, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to change the block status on a record.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function changeBlock()
|
||||
{
|
||||
// Check for request forgeries.
|
||||
$this->checkToken();
|
||||
|
||||
$ids = (array) $this->input->get('cid', [], 'int');
|
||||
$values = ['block' => 1, 'unblock' => 0];
|
||||
$task = $this->getTask();
|
||||
$value = ArrayHelper::getValue($values, $task, 0, 'int');
|
||||
|
||||
// Remove zero values resulting from input filter
|
||||
$ids = array_filter($ids);
|
||||
|
||||
if (empty($ids)) {
|
||||
$this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'warning');
|
||||
} else {
|
||||
// Get the model.
|
||||
$model = $this->getModel();
|
||||
|
||||
// Change the state of the records.
|
||||
if (!$model->block($ids, $value)) {
|
||||
$this->setMessage($model->getError(), 'error');
|
||||
} else {
|
||||
if ($value == 1) {
|
||||
$this->setMessage(Text::plural('COM_USERS_N_USERS_BLOCKED', \count($ids)));
|
||||
} elseif ($value == 0) {
|
||||
$this->setMessage(Text::plural('COM_USERS_N_USERS_UNBLOCKED', \count($ids)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->setRedirect('index.php?option=com_users&view=users');
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to activate a record.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function activate()
|
||||
{
|
||||
// Check for request forgeries.
|
||||
$this->checkToken();
|
||||
|
||||
$ids = (array) $this->input->get('cid', [], 'int');
|
||||
|
||||
// Remove zero values resulting from input filter
|
||||
$ids = array_filter($ids);
|
||||
|
||||
if (empty($ids)) {
|
||||
$this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'error');
|
||||
} else {
|
||||
// Get the model.
|
||||
$model = $this->getModel();
|
||||
|
||||
// Change the state of the records.
|
||||
if (!$model->activate($ids)) {
|
||||
$this->setMessage($model->getError(), 'error');
|
||||
} else {
|
||||
$this->setMessage(Text::plural('COM_USERS_N_USERS_ACTIVATED', \count($ids)));
|
||||
}
|
||||
}
|
||||
|
||||
$this->setRedirect('index.php?option=com_users&view=users');
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the number of active users
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function getQuickiconContent()
|
||||
{
|
||||
$model = $this->getModel('Users');
|
||||
|
||||
$model->setState('filter.state', 0);
|
||||
|
||||
$amount = (int) $model->getTotal();
|
||||
|
||||
$result = [];
|
||||
|
||||
$result['amount'] = $amount;
|
||||
$result['sronly'] = Text::plural('COM_USERS_N_QUICKICON_SRONLY', $amount);
|
||||
$result['name'] = Text::plural('COM_USERS_N_QUICKICON', $amount);
|
||||
|
||||
echo new JsonResponse($result);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\DataShape;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* @property string $pre_message Custom HTML to display above the MFA form
|
||||
* @property string $field_type How to render the MFA code field. "input" or "custom".
|
||||
* @property string $input_type The type attribute for the HTML input box. Typically "text" or "password".
|
||||
* @property string $placeholder Placeholder text for the HTML input box. Leave empty if you don't need it.
|
||||
* @property string $label Label to show above the HTML input box. Leave empty if you don't need it.
|
||||
* @property string $html Custom HTML. Only used when field_type = custom.
|
||||
* @property string $post_message Custom HTML to display below the MFA form
|
||||
* @property bool $hide_submit Should I hide the default Submit button?
|
||||
* @property bool $allowEntryBatching Is this method validating against all configured authenticators of this type?
|
||||
* @property string $help_url URL for help content
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class CaptiveRenderOptions extends DataShapeObject
|
||||
{
|
||||
/**
|
||||
* Display a standard HTML5 input field. Use the input_type, placeholder and label properties to set it up.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public const FIELD_INPUT = 'input';
|
||||
|
||||
/**
|
||||
* Display a custom HTML document. Use the html property to set it up.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public const FIELD_CUSTOM = 'custom';
|
||||
|
||||
/**
|
||||
* Custom HTML to display above the MFA form
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $pre_message = '';
|
||||
|
||||
/**
|
||||
* How to render the MFA code field. "input" (HTML input element) or "custom" (custom HTML)
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $field_type = 'input';
|
||||
|
||||
/**
|
||||
* The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $input_type = '';
|
||||
|
||||
/**
|
||||
* Attributes other than type and id which will be added to the HTML input box.
|
||||
*
|
||||
* @var array
|
||||
* @@since 4.2.0
|
||||
*/
|
||||
protected $input_attributes = [];
|
||||
|
||||
/**
|
||||
* Placeholder text for the HTML input box. Leave empty if you don't need it.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $placeholder = '';
|
||||
|
||||
/**
|
||||
* Label to show above the HTML input box. Leave empty if you don't need it.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $label = '';
|
||||
|
||||
/**
|
||||
* Custom HTML. Only used when field_type = custom.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $html = '';
|
||||
|
||||
/**
|
||||
* Custom HTML to display below the MFA form
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $post_message = '';
|
||||
|
||||
/**
|
||||
* Should I hide the default Submit button?
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $hide_submit = false;
|
||||
|
||||
/**
|
||||
* Additional CSS classes for the submit button (apply the MFA setup)
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $submit_class = '';
|
||||
|
||||
/**
|
||||
* Icon class to use for the submit button
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $submit_icon = 'icon icon-rightarrow icon-arrow-right';
|
||||
|
||||
/**
|
||||
* Language key to use for the text on the submit button
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $submit_text = 'COM_USERS_MFA_VALIDATE';
|
||||
|
||||
/**
|
||||
* Is this MFA method validating against all configured authenticators of the same type?
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $allowEntryBatching = true;
|
||||
|
||||
/**
|
||||
* URL for help content
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $help_url = '';
|
||||
|
||||
/**
|
||||
* Setter for the field_type property
|
||||
*
|
||||
* @param string $value One of self::FIELD_INPUT, self::FIELD_CUSTOM
|
||||
*
|
||||
* @since 4.2.0
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
// phpcs:ignore
|
||||
protected function setField_type(string $value)
|
||||
{
|
||||
if (!\in_array($value, [self::FIELD_INPUT, self::FIELD_CUSTOM])) {
|
||||
throw new \InvalidArgumentException('Invalid value for property field_type.');
|
||||
}
|
||||
|
||||
$this->field_type = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the input_attributes property.
|
||||
*
|
||||
* @param array $value The value to set
|
||||
*
|
||||
* @return void
|
||||
* @@since 4.2.0
|
||||
*/
|
||||
// phpcs:ignore
|
||||
protected function setInput_attributes(array $value)
|
||||
{
|
||||
$forbiddenAttributes = ['id', 'type', 'name', 'value'];
|
||||
|
||||
foreach ($forbiddenAttributes as $key) {
|
||||
if (isset($value[$key])) {
|
||||
unset($value[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->input_attributes = $value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\DataShape;
|
||||
|
||||
/**
|
||||
* Generic helper for handling data shapes in com_users
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
abstract class DataShapeObject implements \ArrayAccess
|
||||
{
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @param array $array The data to initialise this object with
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function __construct(array $array = [])
|
||||
{
|
||||
if (!\is_array($array) && !($array instanceof self)) {
|
||||
throw new \InvalidArgumentException(sprintf('%s needs an array or a %s object', __METHOD__, __CLASS__));
|
||||
}
|
||||
|
||||
foreach (($array instanceof self) ? $array->asArray() : $array as $k => $v) {
|
||||
$this[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data shape as a key-value array
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function asArray(): array
|
||||
{
|
||||
return get_object_vars($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge another data shape object or key-value array into this object.
|
||||
*
|
||||
* @param array|self $newValues The object or array to merge into self.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function merge($newValues): self
|
||||
{
|
||||
if (!\is_array($newValues) && !($newValues instanceof self)) {
|
||||
throw new \InvalidArgumentException(sprintf('%s needs an array or a %s object', __METHOD__, __CLASS__));
|
||||
}
|
||||
|
||||
foreach (($newValues instanceof self) ? $newValues->asArray() : $newValues as $k => $v) {
|
||||
if (!isset($this->{$k})) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this[$k] = $v;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter
|
||||
*
|
||||
* @param string $name The name of the property to retrieve
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
$methodName = 'get' . ucfirst($name);
|
||||
|
||||
if (method_exists($this, $methodName)) {
|
||||
return $this->{$methodName};
|
||||
}
|
||||
|
||||
if (property_exists($this, $name)) {
|
||||
return $this->{$name};
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException(sprintf('Property %s not found in %s', $name, __CLASS__));
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic Setter
|
||||
*
|
||||
* @param string $name The property to set the value for
|
||||
* @param mixed $value The property value to set it to
|
||||
*
|
||||
* @return mixed
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function __set($name, $value)
|
||||
{
|
||||
$methodName = 'set' . ucfirst($name);
|
||||
|
||||
if (method_exists($this, $methodName)) {
|
||||
return $this->{$methodName}($value);
|
||||
}
|
||||
|
||||
if (property_exists($this, $name)) {
|
||||
$this->{$name} = $value;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException(sprintf('Property %s not found in %s', $name, __CLASS__));
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a property set?
|
||||
*
|
||||
* @param string $name Property name
|
||||
*
|
||||
* @return boolean Does it exist in the object?
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function __isset($name): bool
|
||||
{
|
||||
$methodName = 'get' . ucfirst($name);
|
||||
|
||||
return method_exists($this, $methodName) || property_exists($this, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the property exist (array access)?
|
||||
*
|
||||
* @param string $offset Property name
|
||||
*
|
||||
* @return boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->{$offset});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a property (array access).
|
||||
*
|
||||
* @param string $offset Property name
|
||||
*
|
||||
* @return mixed
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function offsetGet($offset): mixed
|
||||
{
|
||||
return $this->{$offset};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of a property (array access).
|
||||
*
|
||||
* @param string $offset Property name
|
||||
* @param mixed $value Property value
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
$this->{$offset} = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset a property (array access).
|
||||
*
|
||||
* @param string $offset Property name
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
throw new \LogicException(sprintf('You cannot unset members of %s', __CLASS__));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\DataShape;
|
||||
|
||||
use Joomla\Component\Users\Administrator\Table\MfaTable;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* @property string $name Internal code of this MFA Method
|
||||
* @property string $display User-facing name for this MFA Method
|
||||
* @property string $shortinfo Short description of this MFA Method displayed to the user
|
||||
* @property string $image URL to the logo image for this Method
|
||||
* @property bool $canDisable Are we allowed to disable it?
|
||||
* @property bool $allowMultiple Are we allowed to have multiple instances of it per user?
|
||||
* @property string $help_url URL for help content
|
||||
* @property bool $allowEntryBatching Allow authentication against all entries of this MFA Method.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class MethodDescriptor extends DataShapeObject
|
||||
{
|
||||
/**
|
||||
* Internal code of this MFA Method
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $name = '';
|
||||
|
||||
/**
|
||||
* User-facing name for this MFA Method
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $display = '';
|
||||
|
||||
/**
|
||||
* Short description of this MFA Method displayed to the user
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $shortinfo = '';
|
||||
|
||||
/**
|
||||
* URL to the logo image for this Method
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $image = '';
|
||||
|
||||
/**
|
||||
* Are we allowed to disable it?
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $canDisable = true;
|
||||
|
||||
/**
|
||||
* Are we allowed to have multiple instances of it per user?
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $allowMultiple = false;
|
||||
|
||||
/**
|
||||
* URL for help content
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $help_url = '';
|
||||
|
||||
/**
|
||||
* Allow authentication against all entries of this MFA Method.
|
||||
*
|
||||
* Otherwise authentication takes place against a SPECIFIC entry at a time.
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $allowEntryBatching = false;
|
||||
|
||||
/**
|
||||
* Active authentication methods, used internally only
|
||||
*
|
||||
* @var MfaTable[]
|
||||
* @since 4.2.0
|
||||
* @internal
|
||||
*/
|
||||
protected $active = [];
|
||||
|
||||
/**
|
||||
* Adds an active MFA method
|
||||
*
|
||||
* @param MfaTable $record The MFA method record to add
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function addActiveMethod(MfaTable $record)
|
||||
{
|
||||
$this->active[$record->id] = $record;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\DataShape;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Data shape for Method Setup Render Options
|
||||
*
|
||||
* @property string $default_title Default title if you are setting up this MFA Method for the first time
|
||||
* @property string $pre_message Custom HTML to display above the MFA setup form
|
||||
* @property string $table_heading Heading for displayed tabular data. Typically used to display a list of fixed MFA
|
||||
* codes, TOTP setup parameters etc
|
||||
* @property array $tabular_data Any tabular data to display (label => custom HTML). See above
|
||||
* @property array $hidden_data Hidden fields to include in the form (name => value)
|
||||
* @property string $field_type How to render the MFA setup code field. "input" (HTML input element) or "custom"
|
||||
* (custom HTML)
|
||||
* @property string $input_type The type attribute for the HTML input box. Typically "text" or "password". Use any
|
||||
* HTML5 input type.
|
||||
* @property string $input_value Pre-filled value for the HTML input box. Typically used for fixed codes, the fixed
|
||||
* YubiKey ID etc.
|
||||
* @property string $placeholder Placeholder text for the HTML input box. Leave empty if you don't need it.
|
||||
* @property string $label Label to show above the HTML input box. Leave empty if you don't need it.
|
||||
* @property string $html Custom HTML. Only used when field_type = custom.
|
||||
* @property bool $show_submit Should I show the submit button (apply the MFA setup)?
|
||||
* @property string $submit_class Additional CSS classes for the submit button (apply the MFA setup)
|
||||
* @property string $post_message Custom HTML to display below the MFA setup form
|
||||
* @property string $help_url A URL with help content for this Method to display to the user
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class SetupRenderOptions extends DataShapeObject
|
||||
{
|
||||
/**
|
||||
* Display a standard HTML5 input field. Use the input_type, placeholder and label properties to set it up.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public const FIELD_INPUT = 'input';
|
||||
|
||||
/**
|
||||
* Display a custom HTML document. Use the html property to set it up.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public const FIELD_CUSTOM = 'custom';
|
||||
|
||||
/**
|
||||
* Default title if you are setting up this MFA Method for the first time
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $default_title = '';
|
||||
|
||||
/**
|
||||
* Custom HTML to display above the MFA setup form parameters etc
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $pre_message = '';
|
||||
|
||||
/**
|
||||
* Heading for displayed tabular data. Typically used to display a list of fixed MFA codes, TOTP setup
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $table_heading = '';
|
||||
|
||||
/**
|
||||
* Any tabular data to display (label => custom HTML). See above
|
||||
*
|
||||
* @var array
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $tabular_data = [];
|
||||
|
||||
/**
|
||||
* Hidden fields to include in the form (name => value)
|
||||
*
|
||||
* @var array
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $hidden_data = [];
|
||||
|
||||
/**
|
||||
* How to render the MFA setup code field. "input" (HTML input element) or "custom" (custom HTML)
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $field_type = 'input';
|
||||
|
||||
/**
|
||||
* The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $input_type = 'text';
|
||||
|
||||
/**
|
||||
* Attributes other than type and id which will be added to the HTML input box.
|
||||
*
|
||||
* @var array
|
||||
* @@since 4.2.0
|
||||
*/
|
||||
protected $input_attributes = [];
|
||||
|
||||
/**
|
||||
* Pre-filled value for the HTML input box. Typically used for fixed codes, the fixed YubiKey ID etc.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $input_value = '';
|
||||
|
||||
/**
|
||||
* Placeholder text for the HTML input box. Leave empty if you don't need it.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $placeholder = '';
|
||||
|
||||
/**
|
||||
* Label to show above the HTML input box. Leave empty if you don't need it.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $label = '';
|
||||
|
||||
/**
|
||||
* Custom HTML. Only used when field_type = custom.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $html = '';
|
||||
|
||||
/**
|
||||
* Should I show the submit button (apply the MFA setup)?
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $show_submit = true;
|
||||
|
||||
/**
|
||||
* Additional CSS classes for the submit button (apply the MFA setup)
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $submit_class = '';
|
||||
|
||||
/**
|
||||
* Icon class to use for the submit button
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $submit_icon = 'icon icon-ok';
|
||||
|
||||
/**
|
||||
* Language key to use for the text on the submit button
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $submit_text = 'JSAVE';
|
||||
|
||||
/**
|
||||
* Custom HTML to display below the MFA setup form
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $post_message = '';
|
||||
|
||||
/**
|
||||
* A URL with help content for this Method to display to the user
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $help_url = '';
|
||||
|
||||
/**
|
||||
* Setter for the field_type property
|
||||
*
|
||||
* @param string $value One of self::FIELD_INPUT, self::FIELD_CUSTOM
|
||||
*
|
||||
* @since 4.2.0
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
// phpcs:ignore
|
||||
protected function setField_type($value)
|
||||
{
|
||||
if (!\in_array($value, [self::FIELD_INPUT, self::FIELD_CUSTOM])) {
|
||||
throw new \InvalidArgumentException('Invalid value for property field_type.');
|
||||
}
|
||||
|
||||
$this->field_type = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the input_attributes property.
|
||||
*
|
||||
* @param array $value The value to set
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
// phpcs:ignore
|
||||
protected function setInput_attributes(array $value)
|
||||
{
|
||||
$forbiddenAttributes = ['id', 'type', 'name', 'value'];
|
||||
|
||||
foreach ($forbiddenAttributes as $key) {
|
||||
if (isset($value[$key])) {
|
||||
unset($value[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->input_attributes = $value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Dispatcher;
|
||||
|
||||
use Joomla\CMS\Dispatcher\ComponentDispatcher;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* ComponentDispatcher class for com_users
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class Dispatcher extends ComponentDispatcher
|
||||
{
|
||||
/**
|
||||
* Override checkAccess to allow users edit profile without having to have core.manager permission
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
protected function checkAccess()
|
||||
{
|
||||
$task = $this->input->getCmd('task');
|
||||
$view = $this->input->getCmd('view');
|
||||
$layout = $this->input->getCmd('layout');
|
||||
$allowedTasks = ['user.edit', 'user.apply', 'user.save', 'user.cancel'];
|
||||
|
||||
// Allow users to edit their own account
|
||||
if (\in_array($task, $allowedTasks, true) || ($view === 'user' && $layout === 'edit')) {
|
||||
$user = $this->app->getIdentity();
|
||||
$id = $this->input->getInt('id');
|
||||
|
||||
if ((int) $user->id === $id) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Special case: Multi-factor Authentication
|
||||
*
|
||||
* We allow access to all MFA views and tasks. Access control for MFA tasks is performed in
|
||||
* the Controllers since what is allowed depends on who is logged in and whose account you
|
||||
* are trying to modify. Implementing these checks in the Dispatcher would violate the
|
||||
* separation of concerns.
|
||||
*/
|
||||
$allowedViews = ['callback', 'captive', 'method', 'methods'];
|
||||
$isAllowedTask = array_reduce(
|
||||
$allowedViews,
|
||||
function ($carry, $taskPrefix) use ($task) {
|
||||
return $carry || strpos($task ?? '', $taskPrefix . '.') === 0;
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
if (\in_array(strtolower($view ?? ''), $allowedViews) || $isAllowedTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent::checkAccess();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Extension;
|
||||
|
||||
use Joomla\CMS\Component\Router\RouterServiceInterface;
|
||||
use Joomla\CMS\Component\Router\RouterServiceTrait;
|
||||
use Joomla\CMS\Extension\BootableExtensionInterface;
|
||||
use Joomla\CMS\Extension\MVCComponent;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Fields\FieldsServiceInterface;
|
||||
use Joomla\CMS\HTML\HTMLRegistryAwareTrait;
|
||||
use Joomla\Component\Users\Administrator\Service\HTML\Users;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Component class for com_users
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class UsersComponent extends MVCComponent implements BootableExtensionInterface, RouterServiceInterface, FieldsServiceInterface
|
||||
{
|
||||
use RouterServiceTrait;
|
||||
use HTMLRegistryAwareTrait;
|
||||
|
||||
/**
|
||||
* Booting the extension. This is the function to set up the environment of the extension like
|
||||
* registering new class loaders, etc.
|
||||
*
|
||||
* If required, some initial set up can be done from services of the container, eg.
|
||||
* registering HTML services.
|
||||
*
|
||||
* @param ContainerInterface $container The container
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function boot(ContainerInterface $container)
|
||||
{
|
||||
$this->getRegistry()->register('users', new Users());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid section for the given section. If it is not valid then null is returned.
|
||||
*
|
||||
* @param string $section The section to get the mapping for
|
||||
* @param object|null $item The content item or null
|
||||
*
|
||||
* @return string|null The new section or null
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function validateSection($section, $item = null)
|
||||
{
|
||||
if (Factory::getApplication()->isClient('site')) {
|
||||
switch ($section) {
|
||||
case 'registration':
|
||||
case 'profile':
|
||||
return 'user';
|
||||
}
|
||||
}
|
||||
|
||||
if ($section === 'user') {
|
||||
return $section;
|
||||
}
|
||||
|
||||
// We don't know other sections.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns valid contexts.
|
||||
*
|
||||
* @return array Associative array with contexts as keys and translated strings as values
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function getContexts(): array
|
||||
{
|
||||
$language = Factory::getApplication()->getLanguage();
|
||||
$language->load('com_users', JPATH_ADMINISTRATOR);
|
||||
|
||||
return [
|
||||
'com_users.user' => $language->_('COM_USERS'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Field;
|
||||
|
||||
use Joomla\CMS\Access\Access;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Form\Field\ListField;
|
||||
use Joomla\CMS\Helper\UserGroupsHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User Group Parent field..
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class GroupparentField extends ListField
|
||||
{
|
||||
/**
|
||||
* The form field type.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $type = 'GroupParent';
|
||||
|
||||
/**
|
||||
* Method to clean the Usergroup Options from all children starting by a given father
|
||||
*
|
||||
* @param array $userGroupsOptions The usergroup options to clean
|
||||
* @param integer $fatherId The father ID to start with
|
||||
*
|
||||
* @return array The cleaned field options
|
||||
*
|
||||
* @since 3.9.4
|
||||
*/
|
||||
private function cleanOptionsChildrenByFather($userGroupsOptions, $fatherId)
|
||||
{
|
||||
foreach ($userGroupsOptions as $userGroupsOptionsId => $userGroupsOptionsData) {
|
||||
if ((int) $userGroupsOptionsData->parent_id === (int) $fatherId) {
|
||||
unset($userGroupsOptions[$userGroupsOptionsId]);
|
||||
|
||||
$userGroupsOptions = $this->cleanOptionsChildrenByFather($userGroupsOptions, $userGroupsOptionsId);
|
||||
}
|
||||
}
|
||||
|
||||
return $userGroupsOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the field options.
|
||||
*
|
||||
* @return array The field option objects
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
$options = UserGroupsHelper::getInstance()->getAll();
|
||||
$currentGroupId = (int) Factory::getApplication()->getInput()->get('id', 0, 'int');
|
||||
|
||||
// Prevent to set yourself as parent
|
||||
if ($currentGroupId) {
|
||||
unset($options[$currentGroupId]);
|
||||
}
|
||||
|
||||
// We should not remove any groups when we are creating a new group
|
||||
if ($currentGroupId !== 0) {
|
||||
// Prevent parenting direct children and children of children of this item.
|
||||
$options = $this->cleanOptionsChildrenByFather($options, $currentGroupId);
|
||||
}
|
||||
|
||||
$options = array_values($options);
|
||||
$isSuperAdmin = $this->getCurrentUser()->authorise('core.admin');
|
||||
|
||||
// Pad the option text with spaces using depth level as a multiplier.
|
||||
for ($i = 0, $n = \count($options); $i < $n; $i++) {
|
||||
// Show groups only if user is super admin or group is not super admin
|
||||
if ($isSuperAdmin || !Access::checkGroup($options[$i]->id, 'core.admin')) {
|
||||
$options[$i]->value = $options[$i]->id;
|
||||
$options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->title;
|
||||
} else {
|
||||
unset($options[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge any additional options in the XML definition.
|
||||
return array_merge(parent::getOptions(), $options);
|
||||
}
|
||||
}
|
||||
47
administrator/components/com_users/src/Field/LevelsField.php
Normal file
47
administrator/components/com_users/src/Field/LevelsField.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Field;
|
||||
|
||||
use Joomla\CMS\Form\Field\ListField;
|
||||
use Joomla\Component\Users\Administrator\Helper\DebugHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Access Levels field.
|
||||
*
|
||||
* @since 3.6.0
|
||||
*/
|
||||
class LevelsField extends ListField
|
||||
{
|
||||
/**
|
||||
* The form field type.
|
||||
*
|
||||
* @var string
|
||||
* @since 3.6.0
|
||||
*/
|
||||
protected $type = 'Levels';
|
||||
|
||||
/**
|
||||
* Method to get the field options.
|
||||
*
|
||||
* @return array The field option objects
|
||||
*
|
||||
* @since 3.6.0
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
// Merge any additional options in the XML definition.
|
||||
return array_merge(parent::getOptions(), DebugHelper::getLevelsOptions());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Field;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Select modules positions.
|
||||
*
|
||||
* Reuses the same field from com_modules. Don't lose it; reuse it!
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class ModulesPositionField extends \Joomla\Component\Modules\Administrator\Field\ModulesPositionField
|
||||
{
|
||||
}
|
||||
163
administrator/components/com_users/src/Helper/DebugHelper.php
Normal file
163
administrator/components/com_users/src/Helper/DebugHelper.php
Normal file
@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Helper;
|
||||
|
||||
use Joomla\CMS\Access\Access;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\Utilities\ArrayHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Users component debugging helper.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class DebugHelper
|
||||
{
|
||||
/**
|
||||
* Get a list of the components.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public static function getComponents()
|
||||
{
|
||||
// Initialise variable.
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select('name AS text, element AS value')
|
||||
->from('#__extensions')
|
||||
->where('enabled >= 1')
|
||||
->where('type =' . $db->quote('component'));
|
||||
|
||||
$items = $db->setQuery($query)->loadObjectList();
|
||||
|
||||
if (\count($items)) {
|
||||
$lang = Factory::getLanguage();
|
||||
|
||||
foreach ($items as &$item) {
|
||||
// Load language
|
||||
$extension = $item->value;
|
||||
$source = JPATH_ADMINISTRATOR . '/components/' . $extension;
|
||||
$lang->load("$extension.sys", JPATH_ADMINISTRATOR)
|
||||
|| $lang->load("$extension.sys", $source);
|
||||
|
||||
// Translate component name
|
||||
$item->text = Text::_($item->text);
|
||||
}
|
||||
|
||||
// Sort by component name
|
||||
$items = ArrayHelper::sortObjects($items, 'text', 1, true, true);
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of the actions for the component or code actions.
|
||||
*
|
||||
* @param string $component The name of the component.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public static function getDebugActions($component = null)
|
||||
{
|
||||
$actions = [];
|
||||
|
||||
// Try to get actions for the component
|
||||
if (!empty($component)) {
|
||||
$component_actions = Access::getActionsFromFile(JPATH_ADMINISTRATOR . '/components/' . $component . '/access.xml');
|
||||
|
||||
if (!empty($component_actions)) {
|
||||
foreach ($component_actions as &$action) {
|
||||
$descr = (string) $action->title;
|
||||
|
||||
if (!empty($action->description)) {
|
||||
$descr = (string) $action->description;
|
||||
}
|
||||
|
||||
$actions[$action->title] = [$action->name, $descr];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use default actions from configuration if no component selected or component doesn't have actions
|
||||
if (empty($actions)) {
|
||||
$filename = JPATH_ADMINISTRATOR . '/components/com_config/forms/application.xml';
|
||||
|
||||
if (is_file($filename)) {
|
||||
$xml = simplexml_load_file($filename);
|
||||
|
||||
foreach ($xml->children()->fieldset as $fieldset) {
|
||||
if ('permissions' == (string) $fieldset['name']) {
|
||||
foreach ($fieldset->children() as $field) {
|
||||
if ('rules' == (string) $field['name']) {
|
||||
foreach ($field->children() as $action) {
|
||||
$descr = (string) $action['title'];
|
||||
|
||||
if (isset($action['description']) && !empty($action['description'])) {
|
||||
$descr = (string) $action['description'];
|
||||
}
|
||||
|
||||
$actions[(string) $action['title']] = [
|
||||
(string) $action['name'],
|
||||
$descr,
|
||||
];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load language
|
||||
$lang = Factory::getLanguage();
|
||||
$extension = 'com_config';
|
||||
$source = JPATH_ADMINISTRATOR . '/components/' . $extension;
|
||||
|
||||
$lang->load($extension, JPATH_ADMINISTRATOR, null, false, false)
|
||||
|| $lang->load($extension, $source, null, false, false)
|
||||
|| $lang->load($extension, JPATH_ADMINISTRATOR, $lang->getDefault(), false, false)
|
||||
|| $lang->load($extension, $source, $lang->getDefault(), false, false);
|
||||
}
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of filter options for the levels.
|
||||
*
|
||||
* @return array An array of \JHtmlOption elements.
|
||||
*/
|
||||
public static function getLevelsOptions()
|
||||
{
|
||||
// Build the filter options.
|
||||
$options = [];
|
||||
$options[] = HTMLHelper::_('select.option', '1', Text::sprintf('COM_USERS_OPTION_LEVEL_COMPONENT', 1));
|
||||
$options[] = HTMLHelper::_('select.option', '2', Text::sprintf('COM_USERS_OPTION_LEVEL_CATEGORY', 2));
|
||||
$options[] = HTMLHelper::_('select.option', '3', Text::sprintf('COM_USERS_OPTION_LEVEL_DEEPER', 3));
|
||||
$options[] = HTMLHelper::_('select.option', '4', '4');
|
||||
$options[] = HTMLHelper::_('select.option', '5', '5');
|
||||
$options[] = HTMLHelper::_('select.option', '6', '6');
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
362
administrator/components/com_users/src/Helper/Mfa.php
Normal file
362
administrator/components/com_users/src/Helper/Mfa.php
Normal file
@ -0,0 +1,362 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Helper;
|
||||
|
||||
use Exception;
|
||||
use Joomla\CMS\Application\CMSApplication;
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Document\HtmlDocument;
|
||||
use Joomla\CMS\Event\MultiFactor\GetMethod;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
use Joomla\CMS\User\User;
|
||||
use Joomla\CMS\User\UserFactoryInterface;
|
||||
use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor;
|
||||
use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
|
||||
use Joomla\Component\Users\Administrator\Model\MethodsModel;
|
||||
use Joomla\Component\Users\Administrator\Table\MfaTable;
|
||||
use Joomla\Component\Users\Administrator\View\Methods\HtmlView;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
use Joomla\Database\ParameterType;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Helper functions for captive MFA handling
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
abstract class Mfa
|
||||
{
|
||||
/**
|
||||
* Cache of all currently active MFAs
|
||||
*
|
||||
* @var array|null
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected static $allMFAs = null;
|
||||
|
||||
/**
|
||||
* Are we inside the administrator application
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected static $isAdmin = null;
|
||||
|
||||
/**
|
||||
* Get the HTML for the Multi-factor Authentication configuration interface for a user.
|
||||
*
|
||||
* This helper method uses a sort of primitive HMVC to display the com_users' Methods page which
|
||||
* renders the MFA configuration interface.
|
||||
*
|
||||
* @param User $user The user we are going to show the configuration UI for.
|
||||
*
|
||||
* @return string|null The HTML of the UI; null if we cannot / must not show it.
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public static function getConfigurationInterface(User $user): ?string
|
||||
{
|
||||
// Check the conditions
|
||||
if (!self::canShowConfigurationInterface($user)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var CMSApplication $app */
|
||||
$app = Factory::getApplication();
|
||||
|
||||
if (!$app->getInput()->getCmd('option', '') === 'com_users') {
|
||||
$app->getLanguage()->load('com_users');
|
||||
$app->getDocument()
|
||||
->getWebAssetManager()
|
||||
->getRegistry()
|
||||
->addExtensionRegistryFile('com_users');
|
||||
}
|
||||
|
||||
// Get a model
|
||||
/** @var MVCFactoryInterface $factory */
|
||||
$factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory();
|
||||
|
||||
/** @var MethodsModel $methodsModel */
|
||||
$methodsModel = $factory->createModel('Methods', 'Administrator');
|
||||
/** @var BackupcodesModel $methodsModel */
|
||||
$backupCodesModel = $factory->createModel('Backupcodes', 'Administrator');
|
||||
|
||||
// Get a view object
|
||||
$appRoot = $app->isClient('site') ? \JPATH_SITE : \JPATH_ADMINISTRATOR;
|
||||
$prefix = $app->isClient('site') ? 'Site' : 'Administrator';
|
||||
/** @var HtmlView $view */
|
||||
$view = $factory->createView(
|
||||
'Methods',
|
||||
$prefix,
|
||||
'Html',
|
||||
[
|
||||
'base_path' => $appRoot . '/components/com_users',
|
||||
]
|
||||
);
|
||||
$view->setModel($methodsModel, true);
|
||||
/** @noinspection PhpParamsInspection */
|
||||
$view->setModel($backupCodesModel);
|
||||
$view->document = $app->getDocument();
|
||||
$view->returnURL = base64_encode(Uri::getInstance()->toString());
|
||||
$view->user = $user;
|
||||
$view->set('forHMVC', true);
|
||||
$view->setLanguage($app->getLanguage());
|
||||
|
||||
@ob_start();
|
||||
|
||||
try {
|
||||
$view->display();
|
||||
} catch (\Throwable $e) {
|
||||
@ob_end_clean();
|
||||
|
||||
/**
|
||||
* This is intentional! When you are developing a Multi-factor Authentication plugin you
|
||||
* will inevitably mess something up and end up with an error. This would cause the
|
||||
* entire MFA configuration page to disappear. No problem! Set Debug System to Yes in
|
||||
* Global Configuration and you can see the error exception which will help you solve
|
||||
* your problem.
|
||||
*/
|
||||
if (\defined('JDEBUG') && JDEBUG) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return @ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all of the MFA Methods
|
||||
*
|
||||
* @return MethodDescriptor[]
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public static function getMfaMethods(): array
|
||||
{
|
||||
PluginHelper::importPlugin('multifactorauth');
|
||||
|
||||
if (\is_null(self::$allMFAs)) {
|
||||
// Get all the plugin results
|
||||
$event = new GetMethod();
|
||||
$temp = Factory::getApplication()
|
||||
->getDispatcher()
|
||||
->dispatch($event->getName(), $event)
|
||||
->getArgument('result', []);
|
||||
|
||||
// Normalize the results
|
||||
self::$allMFAs = [];
|
||||
|
||||
foreach ($temp as $method) {
|
||||
if (!\is_array($method) && !($method instanceof MethodDescriptor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$method = $method instanceof MethodDescriptor
|
||||
? $method : new MethodDescriptor($method);
|
||||
|
||||
if (empty($method['name'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self::$allMFAs[$method['name']] = $method;
|
||||
}
|
||||
}
|
||||
|
||||
return self::$allMFAs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the current user allowed to add/edit MFA methods for $user?
|
||||
*
|
||||
* This is only allowed if I am adding / editing methods for myself.
|
||||
*
|
||||
* If the target user is a member of any group disallowed to use MFA this will return false.
|
||||
*
|
||||
* @param User|null $user The user you want to know if we're allowed to edit
|
||||
*
|
||||
* @return boolean
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public static function canAddEditMethod(?User $user = null): bool
|
||||
{
|
||||
// Cannot do MFA operations on no user or a guest user.
|
||||
if (\is_null($user) || $user->guest) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the user is in a user group which disallows MFA we cannot allow adding / editing methods.
|
||||
$neverMFAGroups = ComponentHelper::getParams('com_users')->get('neverMFAUserGroups', []);
|
||||
$neverMFAGroups = \is_array($neverMFAGroups) ? $neverMFAGroups : [];
|
||||
|
||||
if (\count(array_intersect($user->getAuthorisedGroups(), $neverMFAGroups))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if this is the same as the logged-in user.
|
||||
$myUser = Factory::getApplication()->getIdentity()
|
||||
?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
|
||||
|
||||
return $myUser->id === $user->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the current user allowed to delete MFA methods / disable MFA for $user?
|
||||
*
|
||||
* This is allowed if:
|
||||
* - The user being queried is the same as the logged-in user
|
||||
* - The logged-in user is a Super User AND the queried user is NOT a Super User.
|
||||
*
|
||||
* Note that Super Users can be edited by their own user only for security reasons. If a Super
|
||||
* User gets locked out they must use the Backup Codes to regain access. If that's not possible,
|
||||
* they will need to delete their records from the `#__user_mfa` table.
|
||||
*
|
||||
* @param User|null $user The user being queried.
|
||||
*
|
||||
* @return boolean
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public static function canDeleteMethod(?User $user = null): bool
|
||||
{
|
||||
// Cannot do MFA operations on no user or a guest user.
|
||||
if (\is_null($user) || $user->guest) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$myUser = Factory::getApplication()->getIdentity()
|
||||
?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
|
||||
|
||||
return $myUser->id === $user->id
|
||||
|| ($myUser->authorise('core.admin') && !$user->authorise('core.admin'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all MFA records for a specific user
|
||||
*
|
||||
* @param int|null $userId User ID. NULL for currently logged in user.
|
||||
*
|
||||
* @return MfaTable[]
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public static function getUserMfaRecords(?int $userId): array
|
||||
{
|
||||
if (empty($userId)) {
|
||||
$user = Factory::getApplication()->getIdentity() ?: Factory::getUser();
|
||||
$userId = $user->id ?: 0;
|
||||
}
|
||||
|
||||
/** @var DatabaseInterface $db */
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__user_mfa'))
|
||||
->where($db->quoteName('user_id') . ' = :user_id')
|
||||
->bind(':user_id', $userId, ParameterType::INTEGER);
|
||||
|
||||
try {
|
||||
$ids = $db->setQuery($query)->loadColumn() ?: [];
|
||||
} catch (\Exception $e) {
|
||||
$ids = [];
|
||||
}
|
||||
|
||||
if (empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var MVCFactoryInterface $factory */
|
||||
$factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory();
|
||||
|
||||
// Map all results to MFA table objects
|
||||
$records = array_map(
|
||||
function ($id) use ($factory) {
|
||||
/** @var MfaTable $record */
|
||||
$record = $factory->createTable('Mfa', 'Administrator');
|
||||
$loaded = $record->load($id);
|
||||
|
||||
return $loaded ? $record : null;
|
||||
},
|
||||
$ids
|
||||
);
|
||||
|
||||
// Let's remove Methods we couldn't decrypt when reading from the database.
|
||||
$hasBackupCodes = false;
|
||||
|
||||
$records = array_filter(
|
||||
$records,
|
||||
function ($record) use (&$hasBackupCodes) {
|
||||
$isValid = !\is_null($record) && (!empty($record->options));
|
||||
|
||||
if ($isValid && ($record->method === 'backupcodes')) {
|
||||
$hasBackupCodes = true;
|
||||
}
|
||||
|
||||
return $isValid;
|
||||
}
|
||||
);
|
||||
|
||||
// If the only Method is backup codes it's as good as having no records
|
||||
if ((\count($records) === 1) && $hasBackupCodes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
/**
|
||||
* Are the conditions for showing the MFA configuration interface met?
|
||||
*
|
||||
* @param User|null $user The user to be configured
|
||||
*
|
||||
* @return boolean
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public static function canShowConfigurationInterface(?User $user = null): bool
|
||||
{
|
||||
// If I have no user to check against that's all the checking I can do.
|
||||
if (empty($user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// I need at least one MFA method plugin for the setup interface to make any sense.
|
||||
$plugins = PluginHelper::getPlugin('multifactorauth');
|
||||
|
||||
if (\count($plugins) < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var CMSApplication $app */
|
||||
$app = Factory::getApplication();
|
||||
|
||||
// We can only show a configuration page in the front- or backend application.
|
||||
if (!$app->isClient('site') && !$app->isClient('administrator')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only show the configuration page if we have an HTML document
|
||||
if (!($app->getDocument() instanceof HtmlDocument)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// I must be able to add, edit or delete the user's MFA settings
|
||||
return self::canAddEditMethod($user) || self::canDeleteMethod($user);
|
||||
}
|
||||
}
|
||||
192
administrator/components/com_users/src/Helper/UsersHelper.php
Normal file
192
administrator/components/com_users/src/Helper/UsersHelper.php
Normal file
@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Helper;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Helper\ContentHelper;
|
||||
use Joomla\CMS\Helper\UserGroupsHelper;
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Object\CMSObject;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Users component helper.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class UsersHelper extends ContentHelper
|
||||
{
|
||||
/**
|
||||
* @var CMSObject A cache for the available actions.
|
||||
* @since 1.6
|
||||
*/
|
||||
protected static $actions;
|
||||
|
||||
/**
|
||||
* Get a list of filter options for the blocked state of a user.
|
||||
*
|
||||
* @return array An array of \JHtmlOption elements.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public static function getStateOptions()
|
||||
{
|
||||
// Build the filter options.
|
||||
$options = [];
|
||||
$options[] = HTMLHelper::_('select.option', '0', Text::_('JENABLED'));
|
||||
$options[] = HTMLHelper::_('select.option', '1', Text::_('JDISABLED'));
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of filter options for the activated state of a user.
|
||||
*
|
||||
* @return array An array of \JHtmlOption elements.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public static function getActiveOptions()
|
||||
{
|
||||
// Build the filter options.
|
||||
$options = [];
|
||||
$options[] = HTMLHelper::_('select.option', '0', Text::_('COM_USERS_ACTIVATED'));
|
||||
$options[] = HTMLHelper::_('select.option', '1', Text::_('COM_USERS_UNACTIVATED'));
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of the user groups for filtering.
|
||||
*
|
||||
* @return array An array of \JHtmlOption elements.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public static function getGroups()
|
||||
{
|
||||
$options = UserGroupsHelper::getInstance()->getAll();
|
||||
|
||||
foreach ($options as &$option) {
|
||||
$option->value = $option->id;
|
||||
$option->text = str_repeat('- ', $option->level) . $option->title;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of range options used in filter select list
|
||||
* used in com_users on users view
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
public static function getRangeOptions()
|
||||
{
|
||||
$options = [
|
||||
HTMLHelper::_('select.option', 'today', Text::_('COM_USERS_OPTION_RANGE_TODAY')),
|
||||
HTMLHelper::_('select.option', 'past_week', Text::_('COM_USERS_OPTION_RANGE_PAST_WEEK')),
|
||||
HTMLHelper::_('select.option', 'past_1month', Text::_('COM_USERS_OPTION_RANGE_PAST_1MONTH')),
|
||||
HTMLHelper::_('select.option', 'past_3month', Text::_('COM_USERS_OPTION_RANGE_PAST_3MONTH')),
|
||||
HTMLHelper::_('select.option', 'past_6month', Text::_('COM_USERS_OPTION_RANGE_PAST_6MONTH')),
|
||||
HTMLHelper::_('select.option', 'past_year', Text::_('COM_USERS_OPTION_RANGE_PAST_YEAR')),
|
||||
HTMLHelper::_('select.option', 'post_year', Text::_('COM_USERS_OPTION_RANGE_POST_YEAR')),
|
||||
];
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* No longer used.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @throws \Exception
|
||||
*
|
||||
* @deprecated 4.2 will be removed in 6.0
|
||||
* No longer used, will be removed without replacement
|
||||
*/
|
||||
public static function getTwoFactorMethods()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of the User Groups for Viewing Access Levels
|
||||
*
|
||||
* @param string $rules User Groups in JSON format
|
||||
*
|
||||
* @return string $groups Comma separated list of User Groups
|
||||
*
|
||||
* @since 3.6
|
||||
*/
|
||||
public static function getVisibleByGroups($rules)
|
||||
{
|
||||
$rules = json_decode($rules);
|
||||
|
||||
if (!$rules) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('title', 'text'))
|
||||
->from($db->quoteName('#__usergroups'))
|
||||
->whereIn($db->quoteName('id'), $rules);
|
||||
$db->setQuery($query);
|
||||
|
||||
$groups = $db->loadColumn();
|
||||
$groups = implode(', ', $groups);
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid section for users. If it is not valid then null
|
||||
* is returned.
|
||||
*
|
||||
* @param string $section The section to get the mapping for
|
||||
*
|
||||
* @return string|null The new section
|
||||
*
|
||||
* @since 3.7.0
|
||||
* @throws \Exception
|
||||
*
|
||||
* @deprecated 4.3 will be removed in 6.0
|
||||
* Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::validateSection() instead.
|
||||
*/
|
||||
public static function validateSection($section)
|
||||
{
|
||||
return Factory::getApplication()->bootComponent('com_users')->validateSection($section, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns valid contexts
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @deprecated 4.3 will be removed in 6.0
|
||||
* Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::getContexts() instead.
|
||||
*/
|
||||
public static function getContexts()
|
||||
{
|
||||
return Factory::getApplication()->bootComponent('com_users')->getContexts();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Crypt\Crypt;
|
||||
use Joomla\CMS\Date\Date;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
use Joomla\CMS\User\User;
|
||||
use Joomla\Component\Users\Administrator\Table\MfaTable;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Model for managing backup codes
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class BackupcodesModel extends BaseDatabaseModel
|
||||
{
|
||||
/**
|
||||
* Caches the backup codes per user ID
|
||||
*
|
||||
* @var array
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $cache = [];
|
||||
|
||||
/**
|
||||
* Get the backup codes record for the specified user
|
||||
*
|
||||
* @param User|null $user The user in question. Use null for the currently logged in user.
|
||||
*
|
||||
* @return MfaTable|null Record object or null if none is found
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function getBackupCodesRecord(User $user = null): ?MfaTable
|
||||
{
|
||||
// Make sure I have a user
|
||||
if (empty($user)) {
|
||||
$user = $this->getCurrentUser();
|
||||
}
|
||||
|
||||
/** @var MfaTable $record */
|
||||
$record = $this->getTable('Mfa', 'Administrator');
|
||||
$loaded = $record->load(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'method' => 'backupcodes',
|
||||
]
|
||||
);
|
||||
|
||||
if (!$loaded) {
|
||||
$record = null;
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new set of backup codes for the specified user. The generated codes are immediately saved to the
|
||||
* database and the internal cache is updated.
|
||||
*
|
||||
* @param User|null $user Which user to generate codes for?
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function regenerateBackupCodes(User $user = null): void
|
||||
{
|
||||
// Make sure I have a user
|
||||
if (empty($user)) {
|
||||
$user = $this->getCurrentUser();
|
||||
}
|
||||
|
||||
// Generate backup codes
|
||||
$backupCodes = [];
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
// Each backup code is 2 groups of 4 digits
|
||||
$backupCodes[$i] = sprintf('%04u%04u', random_int(0, 9999), random_int(0, 9999));
|
||||
}
|
||||
|
||||
// Save the backup codes to the database and update the cache
|
||||
$this->saveBackupCodes($backupCodes, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the backup codes to the database
|
||||
*
|
||||
* @param array $codes An array of exactly 10 elements
|
||||
* @param User|null $user The user for which to save the backup codes
|
||||
*
|
||||
* @return boolean
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function saveBackupCodes(array $codes, ?User $user = null): bool
|
||||
{
|
||||
// Make sure I have a user
|
||||
if (empty($user)) {
|
||||
$user = $this->getCurrentUser();
|
||||
}
|
||||
|
||||
// Try to load existing backup codes
|
||||
$existingCodes = $this->getBackupCodes($user);
|
||||
$jNow = Date::getInstance();
|
||||
|
||||
/** @var MfaTable $record */
|
||||
$record = $this->getTable('Mfa', 'Administrator');
|
||||
|
||||
if (\is_null($existingCodes)) {
|
||||
$record->reset();
|
||||
|
||||
$newData = [
|
||||
'user_id' => $user->id,
|
||||
'title' => Text::_('COM_USERS_USER_BACKUPCODES'),
|
||||
'method' => 'backupcodes',
|
||||
'default' => 0,
|
||||
'created_on' => $jNow->toSql(),
|
||||
'options' => $codes,
|
||||
];
|
||||
} else {
|
||||
$record->load(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'method' => 'backupcodes',
|
||||
]
|
||||
);
|
||||
|
||||
$newData = [
|
||||
'options' => $codes,
|
||||
];
|
||||
}
|
||||
|
||||
$saved = $record->save($newData);
|
||||
|
||||
if (!$saved) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Finally, update the cache
|
||||
$this->cache[$user->id] = $codes;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the backup codes for the specified user. Cached values will be preferentially returned, therefore you
|
||||
* MUST go through this model's Methods ONLY when dealing with backup codes.
|
||||
*
|
||||
* @param User|null $user The user for which you want the backup codes
|
||||
*
|
||||
* @return array|null The backup codes, or null if they do not exist
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function getBackupCodes(User $user = null): ?array
|
||||
{
|
||||
// Make sure I have a user
|
||||
if (empty($user)) {
|
||||
$user = $this->getCurrentUser();
|
||||
}
|
||||
|
||||
if (isset($this->cache[$user->id])) {
|
||||
return $this->cache[$user->id];
|
||||
}
|
||||
|
||||
// If there is no cached record try to load it from the database
|
||||
$this->cache[$user->id] = null;
|
||||
|
||||
// Try to load the record
|
||||
/** @var MfaTable $record */
|
||||
$record = $this->getTable('Mfa', 'Administrator');
|
||||
$loaded = $record->load(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'method' => 'backupcodes',
|
||||
]
|
||||
);
|
||||
|
||||
if ($loaded) {
|
||||
$this->cache[$user->id] = $record->options;
|
||||
}
|
||||
|
||||
return $this->cache[$user->id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the provided string is a backup code. If it is, it will be removed from the list (replaced with an empty
|
||||
* string) and the codes will be saved to the database. All comparisons are performed in a timing safe manner.
|
||||
*
|
||||
* @param string $code The code to check
|
||||
* @param User|null $user The user to check against
|
||||
*
|
||||
* @return boolean
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function isBackupCode($code, ?User $user = null): bool
|
||||
{
|
||||
// Load the backup codes
|
||||
$codes = $this->getBackupCodes($user) ?: array_fill(0, 10, '');
|
||||
|
||||
// Keep only the numbers in the provided $code
|
||||
$code = filter_var($code, FILTER_SANITIZE_NUMBER_INT);
|
||||
$code = trim($code);
|
||||
|
||||
// Check if the code is in the array. We always check against ten codes to prevent timing attacks which
|
||||
// determine the amount of codes.
|
||||
$result = false;
|
||||
|
||||
// The two arrays let us always add an element to an array, therefore having PHP expend the same amount of time
|
||||
// for the correct code, the incorrect codes and the fake codes.
|
||||
$newArray = [];
|
||||
$dummyArray = [];
|
||||
|
||||
$realLength = \count($codes);
|
||||
$restLength = 10 - $realLength;
|
||||
|
||||
for ($i = 0; $i < $realLength; $i++) {
|
||||
if (hash_equals($codes[$i], $code)) {
|
||||
// This may seem redundant but makes sure both branches of the if-block are isochronous
|
||||
$result = $result || true;
|
||||
$newArray[] = '';
|
||||
$dummyArray[] = $codes[$i];
|
||||
} else {
|
||||
// This may seem redundant but makes sure both branches of the if-block are isochronous
|
||||
$result = $result || false;
|
||||
$dummyArray[] = '';
|
||||
$newArray[] = $codes[$i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an intentional waste of time, symmetrical to the code above, making sure
|
||||
* evaluating each of the total of ten elements takes the same time. This code should never
|
||||
* run UNLESS someone messed up with our backup codes array and it no longer contains 10
|
||||
* elements.
|
||||
*/
|
||||
$otherResult = false;
|
||||
|
||||
$temp1 = '';
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$temp1[$i] = random_int(0, 99999999);
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $restLength; $i++) {
|
||||
if (Crypt::timingSafeCompare($temp1[$i], $code)) {
|
||||
$otherResult = $otherResult || true;
|
||||
$newArray[] = '';
|
||||
$dummyArray[] = $temp1[$i];
|
||||
} else {
|
||||
$otherResult = $otherResult || false;
|
||||
$newArray[] = '';
|
||||
$dummyArray[] = $temp1[$i];
|
||||
}
|
||||
}
|
||||
|
||||
// This last check makes sure than an empty code does not validate
|
||||
$result = $result && !hash_equals('', $code);
|
||||
|
||||
// Save the backup codes
|
||||
$this->saveBackupCodes($newArray, $user);
|
||||
|
||||
// Finally return the result
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
450
administrator/components/com_users/src/Model/CaptiveModel.php
Normal file
450
administrator/components/com_users/src/Model/CaptiveModel.php
Normal file
@ -0,0 +1,450 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Application\CMSApplication;
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Date\Date;
|
||||
use Joomla\CMS\Event\Module;
|
||||
use Joomla\CMS\Event\MultiFactor\Captive;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
use Joomla\CMS\User\User;
|
||||
use Joomla\Component\Users\Administrator\DataShape\CaptiveRenderOptions;
|
||||
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
|
||||
use Joomla\Component\Users\Administrator\Table\MfaTable;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Captive Multi-factor Authentication page's model
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class CaptiveModel extends BaseDatabaseModel
|
||||
{
|
||||
/**
|
||||
* Cache of the names of the currently active MFA Methods
|
||||
*
|
||||
* @var array|null
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $activeMFAMethodNames = null;
|
||||
|
||||
/**
|
||||
* Prevents Joomla from displaying any modules.
|
||||
*
|
||||
* This is implemented with a trick. If you use jdoc tags to load modules the JDocumentRendererHtmlModules
|
||||
* uses JModuleHelper::getModules() to load the list of modules to render. This goes through JModuleHelper::load()
|
||||
* which triggers the onAfterModuleList event after cleaning up the module list from duplicates. By resetting
|
||||
* the list to an empty array we force Joomla to not display any modules.
|
||||
*
|
||||
* Similar code paths are followed by any canonical code which tries to load modules. So even if your template does
|
||||
* not use jdoc tags this code will still work as expected.
|
||||
*
|
||||
* @param CMSApplication|null $app The CMS application to manipulate
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function suppressAllModules(CMSApplication $app = null): void
|
||||
{
|
||||
if (\is_null($app)) {
|
||||
$app = Factory::getApplication();
|
||||
}
|
||||
|
||||
$app->registerEvent('onAfterModuleList', [$this, 'onAfterModuleList']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MFA records for the user which correspond to active plugins
|
||||
*
|
||||
* @param User|null $user The user for which to fetch records. Skip to use the current user.
|
||||
* @param bool $includeBackupCodes Should I include the backup codes record?
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function getRecords(User $user = null, bool $includeBackupCodes = false): array
|
||||
{
|
||||
if (\is_null($user)) {
|
||||
$user = $this->getCurrentUser();
|
||||
}
|
||||
|
||||
// Get the user's MFA records
|
||||
$records = MfaHelper::getUserMfaRecords($user->id);
|
||||
|
||||
// No MFA Methods? Then we obviously don't need to display a Captive login page.
|
||||
if (empty($records)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get the enabled MFA Methods' names
|
||||
$methodNames = $this->getActiveMethodNames();
|
||||
|
||||
// Filter the records based on currently active MFA Methods
|
||||
$ret = [];
|
||||
|
||||
$methodNames[] = 'backupcodes';
|
||||
$methodNames = array_unique($methodNames);
|
||||
|
||||
if (!$includeBackupCodes) {
|
||||
$methodNames = array_filter(
|
||||
$methodNames,
|
||||
function ($method) {
|
||||
return $method != 'backupcodes';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($records as $record) {
|
||||
// Backup codes must not be included in the list. We add them in the View, at the end of the list.
|
||||
if (\in_array($record->method, $methodNames)) {
|
||||
$ret[$record->id] = $record;
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the active MFA Methods' names
|
||||
*
|
||||
* @return array
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function getActiveMethodNames(): ?array
|
||||
{
|
||||
if (!\is_null($this->activeMFAMethodNames)) {
|
||||
return $this->activeMFAMethodNames;
|
||||
}
|
||||
|
||||
// Let's get a list of all currently active MFA Methods
|
||||
$mfaMethods = MfaHelper::getMfaMethods();
|
||||
|
||||
// If no MFA Method is active we can't really display a Captive login page.
|
||||
if (empty($mfaMethods)) {
|
||||
$this->activeMFAMethodNames = [];
|
||||
|
||||
return $this->activeMFAMethodNames;
|
||||
}
|
||||
|
||||
// Get a list of just the Method names
|
||||
$this->activeMFAMethodNames = [];
|
||||
|
||||
foreach ($mfaMethods as $mfaMethod) {
|
||||
$this->activeMFAMethodNames[] = $mfaMethod['name'];
|
||||
}
|
||||
|
||||
return $this->activeMFAMethodNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected MFA record for the current user. If the record ID is empty, it does not correspond to
|
||||
* the currently logged in user or does not correspond to an active plugin null is returned instead.
|
||||
*
|
||||
* @param User|null $user The user for which to fetch records. Skip to use the current user.
|
||||
*
|
||||
* @return MfaTable|null
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function getRecord(?User $user = null): ?MfaTable
|
||||
{
|
||||
$id = (int) $this->getState('record_id', null);
|
||||
|
||||
if ($id <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (\is_null($user)) {
|
||||
$user = $this->getCurrentUser();
|
||||
}
|
||||
|
||||
/** @var MfaTable $record */
|
||||
$record = $this->getTable('Mfa', 'Administrator');
|
||||
$loaded = $record->load(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'id' => $id,
|
||||
]
|
||||
);
|
||||
|
||||
if (!$loaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$methodNames = $this->getActiveMethodNames();
|
||||
|
||||
if (!\in_array($record->method, $methodNames) && ($record->method != 'backupcodes')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the Captive login page render options for a specific MFA record
|
||||
*
|
||||
* @param MfaTable $record The MFA record to process
|
||||
*
|
||||
* @return CaptiveRenderOptions The rendering options
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function loadCaptiveRenderOptions(?MfaTable $record): CaptiveRenderOptions
|
||||
{
|
||||
$renderOptions = new CaptiveRenderOptions();
|
||||
|
||||
if (empty($record)) {
|
||||
return $renderOptions;
|
||||
}
|
||||
|
||||
$event = new Captive($record);
|
||||
$results = Factory::getApplication()
|
||||
->getDispatcher()
|
||||
->dispatch($event->getName(), $event)
|
||||
->getArgument('result', []);
|
||||
|
||||
if (empty($results)) {
|
||||
if ($record->method === 'backupcodes') {
|
||||
return $renderOptions->merge(
|
||||
[
|
||||
'pre_message' => Text::_('COM_USERS_USER_BACKUPCODES_CAPTIVE_PROMPT'),
|
||||
'input_type' => 'number',
|
||||
'label' => Text::_('COM_USERS_USER_BACKUPCODE'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $renderOptions;
|
||||
}
|
||||
|
||||
foreach ($results as $result) {
|
||||
if (empty($result)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $renderOptions->merge($result);
|
||||
}
|
||||
|
||||
return $renderOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title to display in the Captive login page, or an empty string if no title is to be displayed.
|
||||
*
|
||||
* @return string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function getPageTitle(): string
|
||||
{
|
||||
// In the frontend we can choose if we will display a title
|
||||
$showTitle = (bool) ComponentHelper::getParams('com_users')
|
||||
->get('frontend_show_title', 1);
|
||||
|
||||
if (!$showTitle) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return Text::_('COM_USERS_USER_MULTIFACTOR_AUTH');
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a MFA Method's name into its human-readable, display name
|
||||
*
|
||||
* @param string $name The internal MFA Method name
|
||||
*
|
||||
* @return string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function translateMethodName(string $name): string
|
||||
{
|
||||
static $map = null;
|
||||
|
||||
if (!\is_array($map)) {
|
||||
$map = [];
|
||||
$mfaMethods = MfaHelper::getMfaMethods();
|
||||
|
||||
if (!empty($mfaMethods)) {
|
||||
foreach ($mfaMethods as $mfaMethod) {
|
||||
$map[$mfaMethod['name']] = $mfaMethod['display'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($name == 'backupcodes') {
|
||||
return Text::_('COM_USERS_USER_BACKUPCODES');
|
||||
}
|
||||
|
||||
return $map[$name] ?? $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a MFA Method's name into the relative URL if its logo image
|
||||
*
|
||||
* @param string $name The internal MFA Method name
|
||||
*
|
||||
* @return string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function getMethodImage(string $name): string
|
||||
{
|
||||
static $map = null;
|
||||
|
||||
if (!\is_array($map)) {
|
||||
$map = [];
|
||||
$mfaMethods = MfaHelper::getMfaMethods();
|
||||
|
||||
if (!empty($mfaMethods)) {
|
||||
foreach ($mfaMethods as $mfaMethod) {
|
||||
$map[$mfaMethod['name']] = $mfaMethod['image'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($name == 'backupcodes') {
|
||||
return 'media/com_users/images/emergency.svg';
|
||||
}
|
||||
|
||||
return $map[$name] ?? $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the modules list on Joomla! 4.
|
||||
*
|
||||
* Joomla! 4.x is passing an Event object. The first argument of the event object is the array of modules. After
|
||||
* filtering it we have to overwrite the event argument (NOT just return the new list of modules). If a future
|
||||
* version of Joomla! uses immutable events we'll have to use Reflection to do that or Joomla! would have to fix
|
||||
* the way this event is handled, taking its return into account. For now, we just abuse the mutable event
|
||||
* properties - a feature of the event objects we discussed in the Joomla! 4 Working Group back in August 2015.
|
||||
*
|
||||
* @param Module\AfterModuleListEvent $event The Joomla! event object
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function onAfterModuleList(Module\AfterModuleListEvent $event): void
|
||||
{
|
||||
$modules = $event->getModules();
|
||||
|
||||
if (empty($modules)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->filterModules($modules);
|
||||
$event->updateModules($modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the Method which actually filters the sites modules based on the allowed module positions specified by
|
||||
* the user.
|
||||
*
|
||||
* @param array $modules The list of the site's modules. Passed by reference.
|
||||
*
|
||||
* @return void The by-reference value is modified instead.
|
||||
* @since 4.2.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function filterModules(array &$modules): void
|
||||
{
|
||||
$allowedPositions = $this->getAllowedModulePositions();
|
||||
|
||||
if (empty($allowedPositions)) {
|
||||
$modules = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
|
||||
foreach ($modules as $module) {
|
||||
if (\in_array($module->position, $allowedPositions)) {
|
||||
$filtered[] = $module;
|
||||
}
|
||||
}
|
||||
|
||||
$modules = $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of module positions we are allowed to display
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function getAllowedModulePositions(): array
|
||||
{
|
||||
$isAdmin = Factory::getApplication()->isClient('administrator');
|
||||
|
||||
// Load the list of allowed module positions from the component's settings. May be different for front- and back-end
|
||||
$configKey = 'allowed_positions_' . ($isAdmin ? 'backend' : 'frontend');
|
||||
$res = ComponentHelper::getParams('com_users')->get($configKey, []);
|
||||
|
||||
// In the backend we must always add the 'title' module position
|
||||
if ($isAdmin) {
|
||||
$res[] = 'title';
|
||||
$res[] = 'toolbar';
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to check if the mfa method in question has reached it's usage limit
|
||||
*
|
||||
* @param MfaTable $method Mfa method record
|
||||
*
|
||||
* @return boolean true if user can use the method, false if not
|
||||
*
|
||||
* @since 4.3.2
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function checkTryLimit(MfaTable $method)
|
||||
{
|
||||
$params = ComponentHelper::getParams('com_users');
|
||||
$jNow = Date::getInstance();
|
||||
$maxTries = (int) $params->get('mfatrycount', 10);
|
||||
$blockHours = (int) $params->get('mfatrytime', 1);
|
||||
|
||||
$lastTryTime = strtotime($method->last_try) ?: 0;
|
||||
$hoursSinceLastTry = (strtotime(Factory::getDate()->toSql()) - $lastTryTime) / 3600;
|
||||
|
||||
if ($method->last_try !== null && $hoursSinceLastTry > $blockHours) {
|
||||
// If it's been long enough, start a new reset count
|
||||
$method->last_try = null;
|
||||
$method->tries = 0;
|
||||
} elseif ($method->tries < $maxTries) {
|
||||
// If we are under the max count, just increment the counter
|
||||
++$method->tries;
|
||||
$method->last_try = $jNow->toSql();
|
||||
} else {
|
||||
// At this point, we know we have exceeded the maximum resets for the time period
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store changes to try counter and/or the timestamp
|
||||
$method->store();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
271
administrator/components/com_users/src/Model/DebuggroupModel.php
Normal file
271
administrator/components/com_users/src/Model/DebuggroupModel.php
Normal file
@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Access\Access;
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\MVC\Model\ListModel;
|
||||
use Joomla\CMS\Object\CMSObject;
|
||||
use Joomla\Component\Users\Administrator\Helper\DebugHelper;
|
||||
use Joomla\Database\DatabaseQuery;
|
||||
use Joomla\Database\ParameterType;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Methods supporting a list of User ACL permissions
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class DebuggroupModel extends ListModel
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $config An optional associative array of configuration settings.
|
||||
* @param MVCFactoryInterface $factory The factory.
|
||||
*
|
||||
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
|
||||
* @since 3.2
|
||||
*/
|
||||
public function __construct($config = [], MVCFactoryInterface $factory = null)
|
||||
{
|
||||
if (empty($config['filter_fields'])) {
|
||||
$config['filter_fields'] = [
|
||||
'a.title',
|
||||
'component', 'a.name',
|
||||
'a.lft',
|
||||
'a.id',
|
||||
'level_start', 'level_end', 'a.level',
|
||||
];
|
||||
}
|
||||
|
||||
parent::__construct($config, $factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of the actions.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getDebugActions()
|
||||
{
|
||||
$component = $this->getState('filter.component');
|
||||
|
||||
return DebugHelper::getDebugActions($component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override getItems method.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getItems()
|
||||
{
|
||||
$groupId = $this->getState('group_id');
|
||||
|
||||
if (($assets = parent::getItems()) && $groupId) {
|
||||
$actions = $this->getDebugActions();
|
||||
|
||||
foreach ($assets as &$asset) {
|
||||
$asset->checks = [];
|
||||
|
||||
foreach ($actions as $action) {
|
||||
$name = $action[0];
|
||||
$asset->checks[$name] = Access::checkGroup($groupId, $name, $asset->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $assets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to auto-populate the model state.
|
||||
*
|
||||
* Note. Calling getState in this method will result in recursion.
|
||||
*
|
||||
* @param string $ordering An optional ordering field.
|
||||
* @param string $direction An optional direction (asc|desc).
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function populateState($ordering = 'a.lft', $direction = 'asc')
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
|
||||
// Adjust the context to support modal layouts.
|
||||
$layout = $app->getInput()->get('layout', 'default');
|
||||
|
||||
if ($layout) {
|
||||
$this->context .= '.' . $layout;
|
||||
}
|
||||
|
||||
// Load the filter state.
|
||||
$this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string'));
|
||||
$this->setState('group_id', $this->getUserStateFromRequest($this->context . '.group_id', 'group_id', 0, 'int', false));
|
||||
|
||||
$levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd');
|
||||
$this->setState('filter.level_start', $levelStart);
|
||||
|
||||
$value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd');
|
||||
|
||||
if ($value > 0 && $value < $levelStart) {
|
||||
$value = $levelStart;
|
||||
}
|
||||
|
||||
$this->setState('filter.level_end', $value);
|
||||
|
||||
$this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string'));
|
||||
|
||||
// Load the parameters.
|
||||
$params = ComponentHelper::getParams('com_users');
|
||||
$this->setState('params', $params);
|
||||
|
||||
// List state information.
|
||||
parent::populateState($ordering, $direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get a store id based on model configuration state.
|
||||
*
|
||||
* This is necessary because the model is used by the component and
|
||||
* different modules that might need different sets of data or different
|
||||
* ordering requirements.
|
||||
*
|
||||
* @param string $id A prefix for the store id.
|
||||
*
|
||||
* @return string A store id.
|
||||
*/
|
||||
protected function getStoreId($id = '')
|
||||
{
|
||||
// Compile the store id.
|
||||
$id .= ':' . $this->getState('group_id');
|
||||
$id .= ':' . $this->getState('filter.search');
|
||||
$id .= ':' . $this->getState('filter.level_start');
|
||||
$id .= ':' . $this->getState('filter.level_end');
|
||||
$id .= ':' . $this->getState('filter.component');
|
||||
|
||||
return parent::getStoreId($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the group being debugged.
|
||||
*
|
||||
* @return CMSObject
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getGroup()
|
||||
{
|
||||
$groupId = (int) $this->getState('group_id');
|
||||
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName(['id', 'title']))
|
||||
->from($db->quoteName('#__usergroups'))
|
||||
->where($db->quoteName('id') . ' = :id')
|
||||
->bind(':id', $groupId, ParameterType::INTEGER);
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
try {
|
||||
$group = $db->loadObject();
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an SQL query to load the list data.
|
||||
*
|
||||
* @return DatabaseQuery
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function getListQuery()
|
||||
{
|
||||
// Create a new query object.
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true);
|
||||
|
||||
// Select the required fields from the table.
|
||||
$query->select(
|
||||
$this->getState(
|
||||
'list.select',
|
||||
'a.id, a.name, a.title, a.level, a.lft, a.rgt'
|
||||
)
|
||||
);
|
||||
$query->from($db->quoteName('#__assets', 'a'));
|
||||
|
||||
// Filter the items over the search string if set.
|
||||
if ($this->getState('filter.search')) {
|
||||
$search = '%' . trim($this->getState('filter.search')) . '%';
|
||||
|
||||
// Add the clauses to the query.
|
||||
$query->where(
|
||||
'(' . $db->quoteName('a.name') . ' LIKE :name'
|
||||
. ' OR ' . $db->quoteName('a.title') . ' LIKE :title)'
|
||||
)
|
||||
->bind(':name', $search)
|
||||
->bind(':title', $search);
|
||||
}
|
||||
|
||||
// Filter on the start and end levels.
|
||||
$levelStart = (int) $this->getState('filter.level_start');
|
||||
$levelEnd = (int) $this->getState('filter.level_end');
|
||||
|
||||
if ($levelEnd > 0 && $levelEnd < $levelStart) {
|
||||
$levelEnd = $levelStart;
|
||||
}
|
||||
|
||||
if ($levelStart > 0) {
|
||||
$query->where($db->quoteName('a.level') . ' >= :levelStart')
|
||||
->bind(':levelStart', $levelStart, ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
if ($levelEnd > 0) {
|
||||
$query->where($db->quoteName('a.level') . ' <= :levelEnd')
|
||||
->bind(':levelEnd', $levelEnd, ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
// Filter the items over the component if set.
|
||||
if ($this->getState('filter.component')) {
|
||||
$component = $this->getState('filter.component');
|
||||
$lcomponent = $component . '.%';
|
||||
$query->where(
|
||||
'(' . $db->quoteName('a.name') . ' = :component'
|
||||
. ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)'
|
||||
)
|
||||
->bind(':component', $component)
|
||||
->bind(':lcomponent', $lcomponent);
|
||||
}
|
||||
|
||||
// Add the list ordering clause.
|
||||
$query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')));
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
259
administrator/components/com_users/src/Model/DebuguserModel.php
Normal file
259
administrator/components/com_users/src/Model/DebuguserModel.php
Normal file
@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\MVC\Model\ListModel;
|
||||
use Joomla\CMS\User\User;
|
||||
use Joomla\CMS\User\UserFactoryAwareInterface;
|
||||
use Joomla\CMS\User\UserFactoryAwareTrait;
|
||||
use Joomla\Component\Users\Administrator\Helper\DebugHelper;
|
||||
use Joomla\Database\DatabaseQuery;
|
||||
use Joomla\Database\ParameterType;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Methods supporting a list of User ACL permissions
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class DebuguserModel extends ListModel implements UserFactoryAwareInterface
|
||||
{
|
||||
use UserFactoryAwareTrait;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $config An optional associative array of configuration settings.
|
||||
* @param MVCFactoryInterface $factory The factory.
|
||||
*
|
||||
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
|
||||
* @since 3.2
|
||||
*/
|
||||
public function __construct($config = [], MVCFactoryInterface $factory = null)
|
||||
{
|
||||
if (empty($config['filter_fields'])) {
|
||||
$config['filter_fields'] = [
|
||||
'a.title',
|
||||
'component', 'a.name',
|
||||
'a.lft',
|
||||
'a.id',
|
||||
'level_start', 'level_end', 'a.level',
|
||||
];
|
||||
}
|
||||
|
||||
parent::__construct($config, $factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of the actions.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getDebugActions()
|
||||
{
|
||||
$component = $this->getState('filter.component');
|
||||
|
||||
return DebugHelper::getDebugActions($component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override getItems method.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getItems()
|
||||
{
|
||||
$userId = $this->getState('user_id');
|
||||
$user = $this->getUserFactory()->loadUserById($userId);
|
||||
|
||||
if (($assets = parent::getItems()) && $userId) {
|
||||
$actions = $this->getDebugActions();
|
||||
|
||||
foreach ($assets as &$asset) {
|
||||
$asset->checks = [];
|
||||
|
||||
foreach ($actions as $action) {
|
||||
$name = $action[0];
|
||||
$asset->checks[$name] = $user->authorise($name, $asset->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $assets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to auto-populate the model state.
|
||||
*
|
||||
* Note. Calling getState in this method will result in recursion.
|
||||
*
|
||||
* @param string $ordering An optional ordering field.
|
||||
* @param string $direction An optional direction (asc|desc).
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function populateState($ordering = 'a.lft', $direction = 'asc')
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
|
||||
// Adjust the context to support modal layouts.
|
||||
$layout = $app->getInput()->get('layout', 'default');
|
||||
|
||||
if ($layout) {
|
||||
$this->context .= '.' . $layout;
|
||||
}
|
||||
|
||||
// Load the filter state.
|
||||
$this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string'));
|
||||
$this->setState('user_id', $this->getUserStateFromRequest($this->context . '.user_id', 'user_id', 0, 'int', false));
|
||||
|
||||
$levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd');
|
||||
$this->setState('filter.level_start', $levelStart);
|
||||
|
||||
$value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd');
|
||||
|
||||
if ($value > 0 && $value < $levelStart) {
|
||||
$value = $levelStart;
|
||||
}
|
||||
|
||||
$this->setState('filter.level_end', $value);
|
||||
|
||||
$this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string'));
|
||||
|
||||
// Load the parameters.
|
||||
$params = ComponentHelper::getParams('com_users');
|
||||
$this->setState('params', $params);
|
||||
|
||||
// List state information.
|
||||
parent::populateState($ordering, $direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get a store id based on model configuration state.
|
||||
*
|
||||
* This is necessary because the model is used by the component and
|
||||
* different modules that might need different sets of data or different
|
||||
* ordering requirements.
|
||||
*
|
||||
* @param string $id A prefix for the store id.
|
||||
*
|
||||
* @return string A store id.
|
||||
*/
|
||||
protected function getStoreId($id = '')
|
||||
{
|
||||
// Compile the store id.
|
||||
$id .= ':' . $this->getState('user_id');
|
||||
$id .= ':' . $this->getState('filter.search');
|
||||
$id .= ':' . $this->getState('filter.level_start');
|
||||
$id .= ':' . $this->getState('filter.level_end');
|
||||
$id .= ':' . $this->getState('filter.component');
|
||||
|
||||
return parent::getStoreId($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user being debugged.
|
||||
*
|
||||
* @return User
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
$userId = $this->getState('user_id');
|
||||
|
||||
return $this->getUserFactory()->loadUserById($userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an SQL query to load the list data.
|
||||
*
|
||||
* @return DatabaseQuery
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function getListQuery()
|
||||
{
|
||||
// Create a new query object.
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true);
|
||||
|
||||
// Select the required fields from the table.
|
||||
$query->select(
|
||||
$this->getState(
|
||||
'list.select',
|
||||
'a.id, a.name, a.title, a.level, a.lft, a.rgt'
|
||||
)
|
||||
);
|
||||
$query->from($db->quoteName('#__assets', 'a'));
|
||||
|
||||
// Filter the items over the search string if set.
|
||||
if ($this->getState('filter.search')) {
|
||||
$search = '%' . trim($this->getState('filter.search')) . '%';
|
||||
|
||||
// Add the clauses to the query.
|
||||
$query->where(
|
||||
'(' . $db->quoteName('a.name') . ' LIKE :name'
|
||||
. ' OR ' . $db->quoteName('a.title') . ' LIKE :title)'
|
||||
)
|
||||
->bind(':name', $search)
|
||||
->bind(':title', $search);
|
||||
}
|
||||
|
||||
// Filter on the start and end levels.
|
||||
$levelStart = (int) $this->getState('filter.level_start');
|
||||
$levelEnd = (int) $this->getState('filter.level_end');
|
||||
|
||||
if ($levelEnd > 0 && $levelEnd < $levelStart) {
|
||||
$levelEnd = $levelStart;
|
||||
}
|
||||
|
||||
if ($levelStart > 0) {
|
||||
$query->where($db->quoteName('a.level') . ' >= :levelStart')
|
||||
->bind(':levelStart', $levelStart, ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
if ($levelEnd > 0) {
|
||||
$query->where($db->quoteName('a.level') . ' <= :levelEnd')
|
||||
->bind(':levelEnd', $levelEnd, ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
// Filter the items over the component if set.
|
||||
if ($this->getState('filter.component')) {
|
||||
$component = $this->getState('filter.component');
|
||||
$lcomponent = $component . '.%';
|
||||
$query->where(
|
||||
'(' . $db->quoteName('a.name') . ' = :component'
|
||||
. ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)'
|
||||
)
|
||||
->bind(':component', $component)
|
||||
->bind(':lcomponent', $lcomponent);
|
||||
}
|
||||
|
||||
// Add the list ordering clause.
|
||||
$query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')));
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
347
administrator/components/com_users/src/Model/GroupModel.php
Normal file
347
administrator/components/com_users/src/Model/GroupModel.php
Normal file
@ -0,0 +1,347 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Access\Access;
|
||||
use Joomla\CMS\Event\User\UserGroupAfterDeleteEvent;
|
||||
use Joomla\CMS\Event\User\UserGroupBeforeDeleteEvent;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Form\Form;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\MVC\Model\AdminModel;
|
||||
use Joomla\CMS\Object\CMSObject;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\CMS\Table\Table;
|
||||
use Joomla\String\StringHelper;
|
||||
use Joomla\Utilities\ArrayHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User group model.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class GroupModel extends AdminModel
|
||||
{
|
||||
/**
|
||||
* Override parent constructor.
|
||||
*
|
||||
* @param array $config An optional associative array of configuration settings.
|
||||
* @param MVCFactoryInterface $factory The factory.
|
||||
*
|
||||
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
|
||||
* @since 3.2
|
||||
*/
|
||||
public function __construct($config = [], MVCFactoryInterface $factory = null)
|
||||
{
|
||||
$config = array_merge(
|
||||
[
|
||||
'event_after_delete' => 'onUserAfterDeleteGroup',
|
||||
'event_after_save' => 'onUserAfterSaveGroup',
|
||||
'event_before_delete' => 'onUserBeforeDeleteGroup',
|
||||
'event_before_save' => 'onUserBeforeSaveGroup',
|
||||
'events_map' => ['delete' => 'user', 'save' => 'user'],
|
||||
],
|
||||
$config
|
||||
);
|
||||
|
||||
parent::__construct($config, $factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to the a Table object, always creating it.
|
||||
*
|
||||
* @param string $type The table type to instantiate
|
||||
* @param string $prefix A prefix for the table class name. Optional.
|
||||
* @param array $config Configuration array for model. Optional.
|
||||
*
|
||||
* @return Table A database object
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getTable($type = 'Usergroup', $prefix = 'Joomla\\CMS\\Table\\', $config = [])
|
||||
{
|
||||
$return = Table::getInstance($type, $prefix, $config);
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the record form.
|
||||
*
|
||||
* @param array $data An optional array of data for the form to interrogate.
|
||||
* @param boolean $loadData True if the form is to load its own data (default case), false if not.
|
||||
*
|
||||
* @return Form|bool A Form object on success, false on failure
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getForm($data = [], $loadData = true)
|
||||
{
|
||||
// Get the form.
|
||||
$form = $this->loadForm('com_users.group', 'group', ['control' => 'jform', 'load_data' => $loadData]);
|
||||
|
||||
if (empty($form)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the data that should be injected in the form.
|
||||
*
|
||||
* @return mixed The data for the form.
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function loadFormData()
|
||||
{
|
||||
// Check the session for previously entered form data.
|
||||
$data = Factory::getApplication()->getUserState('com_users.edit.group.data', []);
|
||||
|
||||
if (empty($data)) {
|
||||
$data = $this->getItem();
|
||||
}
|
||||
|
||||
$this->preprocessData('com_users.group', $data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override preprocessForm to load the user plugin group instead of content.
|
||||
*
|
||||
* @param Form $form A form object.
|
||||
* @param mixed $data The data expected for the form.
|
||||
* @param string $group The name of the plugin group to import (defaults to "content").
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception if there is an error loading the form.
|
||||
*/
|
||||
protected function preprocessForm(Form $form, $data, $group = '')
|
||||
{
|
||||
$obj = \is_array($data) ? ArrayHelper::toObject($data, CMSObject::class) : $data;
|
||||
|
||||
if (isset($obj->parent_id) && $obj->parent_id == 0 && $obj->id > 0) {
|
||||
$form->setFieldAttribute('parent_id', 'type', 'hidden');
|
||||
$form->setFieldAttribute('parent_id', 'hidden', 'true');
|
||||
}
|
||||
|
||||
parent::preprocessForm($form, $data, 'user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to save the form data.
|
||||
*
|
||||
* @param array $data The form data.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function save($data)
|
||||
{
|
||||
// Include the user plugins for events.
|
||||
PluginHelper::importPlugin($this->events_map['save']);
|
||||
|
||||
/**
|
||||
* Check the super admin permissions for group
|
||||
* We get the parent group permissions and then check the group permissions manually
|
||||
* We have to calculate the group permissions manually because we haven't saved the group yet
|
||||
*/
|
||||
$parentSuperAdmin = Access::checkGroup($data['parent_id'], 'core.admin');
|
||||
|
||||
// Get core.admin rules from the root asset
|
||||
$rules = Access::getAssetRules('root.1')->getData();
|
||||
|
||||
// Get the value for the current group (will be true (allowed), false (denied), or null (inherit)
|
||||
$groupSuperAdmin = $rules['core.admin']->allow($data['id']);
|
||||
|
||||
// We only need to change the $groupSuperAdmin if the parent is true or false. Otherwise, the value set in the rule takes effect.
|
||||
if ($parentSuperAdmin === false) {
|
||||
// If parent is false (Denied), effective value will always be false
|
||||
$groupSuperAdmin = false;
|
||||
} elseif ($parentSuperAdmin === true) {
|
||||
// If parent is true (allowed), group is true unless explicitly set to false
|
||||
$groupSuperAdmin = ($groupSuperAdmin === false) ? false : true;
|
||||
}
|
||||
|
||||
// Check for non-super admin trying to save with super admin group
|
||||
$iAmSuperAdmin = $this->getCurrentUser()->authorise('core.admin');
|
||||
|
||||
if (!$iAmSuperAdmin && $groupSuperAdmin) {
|
||||
$this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN'));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for super-admin changing self to be non-super-admin
|
||||
* First, are we a super admin
|
||||
*/
|
||||
if ($iAmSuperAdmin) {
|
||||
// Next, are we a member of the current group?
|
||||
$myGroups = Access::getGroupsByUser($this->getCurrentUser()->get('id'), false);
|
||||
|
||||
if (\in_array($data['id'], $myGroups)) {
|
||||
// Now, would we have super admin permissions without the current group?
|
||||
$otherGroups = array_diff($myGroups, [$data['id']]);
|
||||
$otherSuperAdmin = false;
|
||||
|
||||
foreach ($otherGroups as $otherGroup) {
|
||||
$otherSuperAdmin = $otherSuperAdmin ?: Access::checkGroup($otherGroup, 'core.admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* If we would not otherwise have super admin permissions
|
||||
* and the current group does not have super admin permissions, throw an exception
|
||||
*/
|
||||
if ((!$otherSuperAdmin) && (!$groupSuperAdmin)) {
|
||||
$this->setError(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF'));
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Factory::getApplication()->getInput()->get('task') == 'save2copy') {
|
||||
$data['title'] = $this->generateGroupTitle($data['parent_id'], $data['title']);
|
||||
}
|
||||
|
||||
// Proceed with the save
|
||||
return parent::save($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to delete rows.
|
||||
*
|
||||
* @param array &$pks An array of item ids.
|
||||
*
|
||||
* @return boolean Returns true on success, false on failure.
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function delete(&$pks)
|
||||
{
|
||||
// Typecast variable.
|
||||
$pks = (array) $pks;
|
||||
$user = $this->getCurrentUser();
|
||||
$groups = Access::getGroupsByUser($user->get('id'));
|
||||
$context = $this->option . '.' . $this->name;
|
||||
$dispatcher = $this->getDispatcher();
|
||||
|
||||
// Get a row instance.
|
||||
$table = $this->getTable();
|
||||
|
||||
// Load plugins.
|
||||
PluginHelper::importPlugin($this->events_map['delete'], null, true, $dispatcher);
|
||||
|
||||
// Check if I am a Super Admin
|
||||
$iAmSuperAdmin = $user->authorise('core.admin');
|
||||
|
||||
foreach ($pks as $pk) {
|
||||
// Do not allow to delete groups to which the current user belongs
|
||||
if (\in_array($pk, $groups)) {
|
||||
Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_DELETE_ERROR_INVALID_GROUP'), 'error');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$table->load($pk)) {
|
||||
// Item is not in the table.
|
||||
$this->setError($table->getError());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate the items to delete each one.
|
||||
foreach ($pks as $i => $pk) {
|
||||
if ($table->load($pk)) {
|
||||
// Access checks.
|
||||
$allow = $user->authorise('core.edit.state', 'com_users');
|
||||
|
||||
// Don't allow non-super-admin to delete a super admin
|
||||
$allow = (!$iAmSuperAdmin && Access::checkGroup($pk, 'core.admin')) ? false : $allow;
|
||||
|
||||
if ($allow) {
|
||||
// Fire the before delete event.
|
||||
$beforeDeleteEvent = new UserGroupBeforeDeleteEvent($this->event_before_delete, [
|
||||
'data' => $table->getProperties(), // @TODO: Remove data argument in Joomla 6, see UserGroupBeforeDeleteEvent
|
||||
'context' => $context,
|
||||
'subject' => $table,
|
||||
]);
|
||||
$result = $dispatcher->dispatch($this->event_before_delete, $beforeDeleteEvent)->getArgument('result', []);
|
||||
|
||||
if (\in_array(false, $result, true)) {
|
||||
$this->setError($table->getError());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$table->delete($pk)) {
|
||||
$this->setError($table->getError());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Trigger the after delete event.
|
||||
$dispatcher->dispatch($this->event_after_delete, new UserGroupAfterDeleteEvent($this->event_after_delete, [
|
||||
'data' => $table->getProperties(), // @TODO: Remove data argument in Joomla 6, see UserGroupAfterDeleteEvent
|
||||
'deletingResult' => true, // @TODO: Remove deletingResult argument in Joomla 6, see UserGroupAfterDeleteEvent
|
||||
'errorMessage' => $this->getError(), // @TODO: Remove errorMessage argument in Joomla 6, see UserGroupAfterDeleteEvent
|
||||
'context' => $context,
|
||||
'subject' => $table,
|
||||
]));
|
||||
} else {
|
||||
// Prune items that you can't change.
|
||||
unset($pks[$i]);
|
||||
Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to generate the title of group on Save as Copy action
|
||||
*
|
||||
* @param integer $parentId The id of the parent.
|
||||
* @param string $title The title of group
|
||||
*
|
||||
* @return string Contains the modified title.
|
||||
*
|
||||
* @since 3.3.7
|
||||
*/
|
||||
protected function generateGroupTitle($parentId, $title)
|
||||
{
|
||||
// Alter the title & alias
|
||||
$table = $this->getTable();
|
||||
|
||||
while ($table->load(['title' => $title, 'parent_id' => $parentId])) {
|
||||
if ($title == $table->title) {
|
||||
$title = StringHelper::increment($title);
|
||||
}
|
||||
}
|
||||
|
||||
return $title;
|
||||
}
|
||||
}
|
||||
246
administrator/components/com_users/src/Model/GroupsModel.php
Normal file
246
administrator/components/com_users/src/Model/GroupsModel.php
Normal file
@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Helper\UserGroupsHelper;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\MVC\Model\ListModel;
|
||||
use Joomla\Database\DatabaseQuery;
|
||||
use Joomla\Database\ParameterType;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Methods supporting a list of user group records.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class GroupsModel extends ListModel
|
||||
{
|
||||
/**
|
||||
* Override parent constructor.
|
||||
*
|
||||
* @param array $config An optional associative array of configuration settings.
|
||||
* @param MVCFactoryInterface $factory The factory.
|
||||
*
|
||||
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
|
||||
* @since 3.2
|
||||
*/
|
||||
public function __construct($config = [], MVCFactoryInterface $factory = null)
|
||||
{
|
||||
if (empty($config['filter_fields'])) {
|
||||
$config['filter_fields'] = [
|
||||
'id', 'a.id',
|
||||
'parent_id', 'a.parent_id',
|
||||
'title', 'a.title',
|
||||
'lft', 'a.lft',
|
||||
'rgt', 'a.rgt',
|
||||
];
|
||||
}
|
||||
|
||||
parent::__construct($config, $factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to auto-populate the model state.
|
||||
*
|
||||
* Note. Calling getState in this method will result in recursion.
|
||||
*
|
||||
* @param string $ordering An optional ordering field.
|
||||
* @param string $direction An optional direction (asc|desc).
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function populateState($ordering = 'a.lft', $direction = 'asc')
|
||||
{
|
||||
// Load the parameters.
|
||||
$params = ComponentHelper::getParams('com_users');
|
||||
$this->setState('params', $params);
|
||||
|
||||
// List state information.
|
||||
parent::populateState($ordering, $direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get a store id based on model configuration state.
|
||||
*
|
||||
* This is necessary because the model is used by the component and
|
||||
* different modules that might need different sets of data or different
|
||||
* ordering requirements.
|
||||
*
|
||||
* @param string $id A prefix for the store id.
|
||||
*
|
||||
* @return string A store id.
|
||||
*/
|
||||
protected function getStoreId($id = '')
|
||||
{
|
||||
// Compile the store id.
|
||||
$id .= ':' . $this->getState('filter.search');
|
||||
|
||||
return parent::getStoreId($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of groups and adds expensive joins to the result set.
|
||||
*
|
||||
* @return mixed An array of data items on success, false on failure.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getItems()
|
||||
{
|
||||
// Get a storage key.
|
||||
$store = $this->getStoreId();
|
||||
|
||||
// Try to load the data from internal storage.
|
||||
if (empty($this->cache[$store])) {
|
||||
$items = parent::getItems();
|
||||
|
||||
// Bail out on an error or empty list.
|
||||
if (empty($items)) {
|
||||
$this->cache[$store] = $items;
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
try {
|
||||
$items = $this->populateExtraData($items);
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the items to the internal cache.
|
||||
$this->cache[$store] = $items;
|
||||
}
|
||||
|
||||
return $this->cache[$store];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an SQL query to load the list data.
|
||||
*
|
||||
* @return DatabaseQuery
|
||||
*/
|
||||
protected function getListQuery()
|
||||
{
|
||||
// Create a new query object.
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true);
|
||||
|
||||
// Select the required fields from the table.
|
||||
$query->select(
|
||||
$this->getState(
|
||||
'list.select',
|
||||
'a.*'
|
||||
)
|
||||
);
|
||||
$query->from($db->quoteName('#__usergroups') . ' AS a');
|
||||
|
||||
// Filter the comments over the search string if set.
|
||||
$search = $this->getState('filter.search');
|
||||
|
||||
if (!empty($search)) {
|
||||
if (stripos($search, 'id:') === 0) {
|
||||
$ids = (int) substr($search, 3);
|
||||
$query->where($db->quoteName('a.id') . ' = :id');
|
||||
$query->bind(':id', $ids, ParameterType::INTEGER);
|
||||
} else {
|
||||
$search = '%' . trim($search) . '%';
|
||||
$query->where($db->quoteName('a.title') . ' LIKE :title');
|
||||
$query->bind(':title', $search);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the list ordering clause.
|
||||
$query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')));
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate level & path for items.
|
||||
*
|
||||
* @param array $items Array of \stdClass objects
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 3.6.3
|
||||
*/
|
||||
private function populateExtraData(array $items)
|
||||
{
|
||||
// First pass: get list of the group ids and reset the counts.
|
||||
$groupsByKey = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$groupsByKey[(int) $item->id] = $item;
|
||||
}
|
||||
|
||||
$groupIds = array_keys($groupsByKey);
|
||||
|
||||
$db = $this->getDatabase();
|
||||
|
||||
// Get total enabled users in group.
|
||||
$query = $db->getQuery(true);
|
||||
|
||||
// Count the objects in the user group.
|
||||
$query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count')
|
||||
->from($db->quoteName('#__user_usergroup_map', 'map'))
|
||||
->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id'))
|
||||
->whereIn($db->quoteName('map.group_id'), $groupIds)
|
||||
->where($db->quoteName('u.block') . ' = 0')
|
||||
->group($db->quoteName('map.group_id'));
|
||||
$db->setQuery($query);
|
||||
|
||||
try {
|
||||
$countEnabled = $db->loadAssocList('group_id', 'count_enabled');
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get total disabled users in group.
|
||||
$query->clear();
|
||||
$query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count')
|
||||
->from($db->quoteName('#__user_usergroup_map', 'map'))
|
||||
->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id'))
|
||||
->whereIn($db->quoteName('map.group_id'), $groupIds)
|
||||
->where($db->quoteName('u.block') . ' = 1')
|
||||
->group($db->quoteName('map.group_id'));
|
||||
$db->setQuery($query);
|
||||
|
||||
try {
|
||||
$countDisabled = $db->loadAssocList('group_id', 'count_disabled');
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Inject the values back into the array.
|
||||
foreach ($groupsByKey as &$item) {
|
||||
$item->count_enabled = isset($countEnabled[$item->id]) ? (int) $countEnabled[$item->id]['user_count'] : 0;
|
||||
$item->count_disabled = isset($countDisabled[$item->id]) ? (int) $countDisabled[$item->id]['user_count'] : 0;
|
||||
$item->user_count = $item->count_enabled + $item->count_disabled;
|
||||
}
|
||||
|
||||
$groups = new UserGroupsHelper($groupsByKey);
|
||||
|
||||
return array_values($groups->getAll());
|
||||
}
|
||||
}
|
||||
297
administrator/components/com_users/src/Model/LevelModel.php
Normal file
297
administrator/components/com_users/src/Model/LevelModel.php
Normal file
@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Access\Access;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Filter\InputFilter;
|
||||
use Joomla\CMS\Form\Form;
|
||||
use Joomla\CMS\Helper\UserGroupsHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Model\AdminModel;
|
||||
use Joomla\CMS\Table\Table;
|
||||
use Joomla\Utilities\ArrayHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User view level model.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class LevelModel extends AdminModel
|
||||
{
|
||||
/**
|
||||
* @var array A list of the access levels in use.
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $levelsInUse = null;
|
||||
|
||||
/**
|
||||
* Method to test whether a record can be deleted.
|
||||
*
|
||||
* @param object $record A record object.
|
||||
*
|
||||
* @return boolean True if allowed to delete the record. Defaults to the permission set in the component.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function canDelete($record)
|
||||
{
|
||||
$groups = json_decode($record->rules);
|
||||
|
||||
if ($groups === null) {
|
||||
throw new \RuntimeException('Invalid rules schema');
|
||||
}
|
||||
|
||||
$isAdmin = $this->getCurrentUser()->authorise('core.admin');
|
||||
|
||||
// Check permissions
|
||||
foreach ($groups as $group) {
|
||||
if (!$isAdmin && Access::checkGroup($group, 'core.admin')) {
|
||||
$this->setError(Text::_('JERROR_ALERTNOAUTHOR'));
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the access level is being used by any content.
|
||||
if ($this->levelsInUse === null) {
|
||||
// Populate the list once.
|
||||
$this->levelsInUse = [];
|
||||
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true)
|
||||
->select('DISTINCT access');
|
||||
|
||||
// Get all the tables and the prefix
|
||||
$tables = $db->getTableList();
|
||||
$prefix = $db->getPrefix();
|
||||
|
||||
foreach ($tables as $table) {
|
||||
// Get all of the columns in the table
|
||||
$fields = $db->getTableColumns($table);
|
||||
|
||||
/**
|
||||
* We are looking for the access field. If custom tables are using something other
|
||||
* than the 'access' field they are on their own unfortunately.
|
||||
* Also make sure the table prefix matches the live db prefix (eg, it is not a "bak_" table)
|
||||
*/
|
||||
if (strpos($table, $prefix) === 0 && isset($fields['access'])) {
|
||||
// Lookup the distinct values of the field.
|
||||
$query->clear('from')
|
||||
->from($db->quoteName($table));
|
||||
$db->setQuery($query);
|
||||
|
||||
try {
|
||||
$values = $db->loadColumn();
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->levelsInUse = array_merge($this->levelsInUse, $values);
|
||||
|
||||
// @todo Could assemble an array of the tables used by each view level list those,
|
||||
// giving the user a clue in the error where to look.
|
||||
}
|
||||
}
|
||||
|
||||
// Get uniques.
|
||||
$this->levelsInUse = array_unique($this->levelsInUse);
|
||||
|
||||
// Ok, after all that we are ready to check the record :)
|
||||
}
|
||||
|
||||
if (\in_array($record->id, $this->levelsInUse)) {
|
||||
$this->setError(Text::sprintf('COM_USERS_ERROR_VIEW_LEVEL_IN_USE', $record->id, $record->title));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canDelete($record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to the a Table object, always creating it.
|
||||
*
|
||||
* @param string $type The table type to instantiate
|
||||
* @param string $prefix A prefix for the table class name. Optional.
|
||||
* @param array $config Configuration array for model. Optional.
|
||||
*
|
||||
* @return Table A database object
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getTable($type = 'ViewLevel', $prefix = 'Joomla\\CMS\\Table\\', $config = [])
|
||||
{
|
||||
$return = Table::getInstance($type, $prefix, $config);
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get a single record.
|
||||
*
|
||||
* @param integer $pk The id of the primary key.
|
||||
*
|
||||
* @return mixed Object on success, false on failure.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getItem($pk = null)
|
||||
{
|
||||
$result = parent::getItem($pk);
|
||||
|
||||
// Convert the params field to an array.
|
||||
$result->rules = $result->rules !== null ? json_decode($result->rules) : [];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the record form.
|
||||
*
|
||||
* @param array $data An optional array of data for the form to interrogate.
|
||||
* @param boolean $loadData True if the form is to load its own data (default case), false if not.
|
||||
*
|
||||
* @return Form|bool A Form object on success, false on failure
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getForm($data = [], $loadData = true)
|
||||
{
|
||||
// Get the form.
|
||||
$form = $this->loadForm('com_users.level', 'level', ['control' => 'jform', 'load_data' => $loadData]);
|
||||
|
||||
if (empty($form)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the data that should be injected in the form.
|
||||
*
|
||||
* @return mixed The data for the form.
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function loadFormData()
|
||||
{
|
||||
// Check the session for previously entered form data.
|
||||
$data = Factory::getApplication()->getUserState('com_users.edit.level.data', []);
|
||||
|
||||
if (empty($data)) {
|
||||
$data = $this->getItem();
|
||||
}
|
||||
|
||||
$this->preprocessData('com_users.level', $data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to preprocess the form
|
||||
*
|
||||
* @param Form $form A form object.
|
||||
* @param mixed $data The data expected for the form.
|
||||
* @param string $group The name of the plugin group to import (defaults to "content").
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception if there is an error loading the form.
|
||||
*/
|
||||
protected function preprocessForm(Form $form, $data, $group = '')
|
||||
{
|
||||
// TO DO warning!
|
||||
parent::preprocessForm($form, $data, 'user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to save the form data.
|
||||
*
|
||||
* @param array $data The form data.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function save($data)
|
||||
{
|
||||
if (!isset($data['rules'])) {
|
||||
$data['rules'] = [];
|
||||
}
|
||||
|
||||
$data['title'] = InputFilter::getInstance()->clean($data['title'], 'TRIM');
|
||||
|
||||
return parent::save($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to validate the form data.
|
||||
*
|
||||
* @param Form $form The form to validate against.
|
||||
* @param array $data The data to validate.
|
||||
* @param string $group The name of the field group to validate.
|
||||
*
|
||||
* @return array|boolean Array of filtered data if valid, false otherwise.
|
||||
*
|
||||
* @see \Joomla\CMS\Form\FormRule
|
||||
* @see \Joomla\CMS\Filter\InputFilter
|
||||
* @since 3.8.8
|
||||
*/
|
||||
public function validate($form, $data, $group = null)
|
||||
{
|
||||
$isSuperAdmin = $this->getCurrentUser()->authorise('core.admin');
|
||||
|
||||
// Non Super user should not be able to change the access levels of super user groups
|
||||
if (!$isSuperAdmin) {
|
||||
if (!isset($data['rules']) || !\is_array($data['rules'])) {
|
||||
$data['rules'] = [];
|
||||
}
|
||||
|
||||
$groups = array_values(UserGroupsHelper::getInstance()->getAll());
|
||||
|
||||
$rules = [];
|
||||
|
||||
if (!empty($data['id'])) {
|
||||
$table = $this->getTable();
|
||||
|
||||
$table->load($data['id']);
|
||||
|
||||
$rules = json_decode($table->rules);
|
||||
}
|
||||
|
||||
$rules = ArrayHelper::toInteger($rules);
|
||||
|
||||
for ($i = 0, $n = \count($groups); $i < $n; ++$i) {
|
||||
if (Access::checkGroup((int) $groups[$i]->id, 'core.admin')) {
|
||||
if (\in_array((int) $groups[$i]->id, $rules) && !\in_array((int) $groups[$i]->id, $data['rules'])) {
|
||||
$data['rules'][] = (int) $groups[$i]->id;
|
||||
} elseif (!\in_array((int) $groups[$i]->id, $rules) && \in_array((int) $groups[$i]->id, $data['rules'])) {
|
||||
$this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN'));
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parent::validate($form, $data, $group);
|
||||
}
|
||||
}
|
||||
234
administrator/components/com_users/src/Model/LevelsModel.php
Normal file
234
administrator/components/com_users/src/Model/LevelsModel.php
Normal file
@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\MVC\Model\ListModel;
|
||||
use Joomla\CMS\Table\Table;
|
||||
use Joomla\Database\DatabaseQuery;
|
||||
use Joomla\Database\ParameterType;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Methods supporting a list of user access level records.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class LevelsModel extends ListModel
|
||||
{
|
||||
/**
|
||||
* Override parent constructor.
|
||||
*
|
||||
* @param array $config An optional associative array of configuration settings.
|
||||
* @param MVCFactoryInterface $factory The factory.
|
||||
*
|
||||
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
|
||||
* @since 3.2
|
||||
*/
|
||||
public function __construct($config = [], MVCFactoryInterface $factory = null)
|
||||
{
|
||||
if (empty($config['filter_fields'])) {
|
||||
$config['filter_fields'] = [
|
||||
'id', 'a.id',
|
||||
'title', 'a.title',
|
||||
'ordering', 'a.ordering',
|
||||
];
|
||||
}
|
||||
|
||||
parent::__construct($config, $factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to auto-populate the model state.
|
||||
*
|
||||
* Note. Calling getState in this method will result in recursion.
|
||||
*
|
||||
* @param string $ordering An optional ordering field.
|
||||
* @param string $direction An optional direction (asc|desc).
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function populateState($ordering = 'a.ordering', $direction = 'asc')
|
||||
{
|
||||
// Load the parameters.
|
||||
$params = ComponentHelper::getParams('com_users');
|
||||
$this->setState('params', $params);
|
||||
|
||||
// List state information.
|
||||
parent::populateState($ordering, $direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get a store id based on model configuration state.
|
||||
*
|
||||
* This is necessary because the model is used by the component and
|
||||
* different modules that might need different sets of data or different
|
||||
* ordering requirements.
|
||||
*
|
||||
* @param string $id A prefix for the store id.
|
||||
*
|
||||
* @return string A store id.
|
||||
*/
|
||||
protected function getStoreId($id = '')
|
||||
{
|
||||
// Compile the store id.
|
||||
$id .= ':' . $this->getState('filter.search');
|
||||
|
||||
return parent::getStoreId($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an SQL query to load the list data.
|
||||
*
|
||||
* @return DatabaseQuery
|
||||
*/
|
||||
protected function getListQuery()
|
||||
{
|
||||
// Create a new query object.
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true);
|
||||
|
||||
// Select the required fields from the table.
|
||||
$query->select(
|
||||
$this->getState(
|
||||
'list.select',
|
||||
'a.*'
|
||||
)
|
||||
);
|
||||
$query->from($db->quoteName('#__viewlevels') . ' AS a');
|
||||
|
||||
// Add the level in the tree.
|
||||
$query->group('a.id, a.title, a.ordering, a.rules');
|
||||
|
||||
// Filter the items over the search string if set.
|
||||
$search = $this->getState('filter.search');
|
||||
|
||||
if (!empty($search)) {
|
||||
if (stripos($search, 'id:') === 0) {
|
||||
$ids = (int) substr($search, 3);
|
||||
$query->where($db->quoteName('a.id') . ' = :id');
|
||||
$query->bind(':id', $ids, ParameterType::INTEGER);
|
||||
} else {
|
||||
$search = '%' . trim($search) . '%';
|
||||
$query->where('a.title LIKE :title')
|
||||
->bind(':title', $search);
|
||||
}
|
||||
}
|
||||
|
||||
$query->group('a.id');
|
||||
|
||||
// Add the list ordering clause.
|
||||
$query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')));
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to adjust the ordering of a row.
|
||||
*
|
||||
* @param integer $pk The ID of the primary key to move.
|
||||
* @param integer $direction Increment, usually +1 or -1
|
||||
*
|
||||
* @return boolean False on failure or error, true otherwise.
|
||||
*/
|
||||
public function reorder($pk, $direction = 0)
|
||||
{
|
||||
// Sanitize the id and adjustment.
|
||||
$pk = (!empty($pk)) ? $pk : (int) $this->getState('level.id');
|
||||
$user = $this->getCurrentUser();
|
||||
|
||||
// Get an instance of the record's table.
|
||||
$table = Table::getInstance('ViewLevel', 'Joomla\\CMS\Table\\');
|
||||
|
||||
// Load the row.
|
||||
if (!$table->load($pk)) {
|
||||
$this->setError($table->getError());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Access checks.
|
||||
$allow = $user->authorise('core.edit.state', 'com_users');
|
||||
|
||||
if (!$allow) {
|
||||
$this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Move the row.
|
||||
// @todo: Where clause to restrict category.
|
||||
$table->move($pk);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the manually set order of records.
|
||||
*
|
||||
* @param array $pks An array of primary key ids.
|
||||
* @param integer $order Order position
|
||||
*
|
||||
* @return boolean Boolean true on success, boolean false
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function saveorder($pks, $order)
|
||||
{
|
||||
$table = Table::getInstance('viewlevel', 'Joomla\\CMS\Table\\');
|
||||
$user = $this->getCurrentUser();
|
||||
$conditions = [];
|
||||
|
||||
if (empty($pks)) {
|
||||
Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_ERROR_LEVELS_NOLEVELS_SELECTED'), 'error');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update ordering values
|
||||
foreach ($pks as $i => $pk) {
|
||||
$table->load((int) $pk);
|
||||
|
||||
// Access checks.
|
||||
$allow = $user->authorise('core.edit.state', 'com_users');
|
||||
|
||||
if (!$allow) {
|
||||
// Prune items that you can't change.
|
||||
unset($pks[$i]);
|
||||
Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error');
|
||||
} elseif ($table->ordering != $order[$i]) {
|
||||
$table->ordering = $order[$i];
|
||||
|
||||
if (!$table->store()) {
|
||||
$this->setError($table->getError());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute reorder for each category.
|
||||
foreach ($conditions as $cond) {
|
||||
$table->load($cond[0]);
|
||||
$table->reorder($cond[1]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
246
administrator/components/com_users/src/Model/MailModel.php
Normal file
246
administrator/components/com_users/src/Model/MailModel.php
Normal file
@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Access\Access;
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Filter\InputFilter;
|
||||
use Joomla\CMS\Form\Form;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\CMS\Mail\Exception\MailDisabledException;
|
||||
use Joomla\CMS\Mail\MailTemplate;
|
||||
use Joomla\CMS\MVC\Model\AdminModel;
|
||||
use Joomla\Database\ParameterType;
|
||||
use PHPMailer\PHPMailer\Exception as phpMailerException;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Users mail model.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class MailModel extends AdminModel
|
||||
{
|
||||
/**
|
||||
* Method to get the row form.
|
||||
*
|
||||
* @param array $data An optional array of data for the form to interrogate.
|
||||
* @param boolean $loadData True if the form is to load its own data (default case), false if not.
|
||||
*
|
||||
* @return Form A Form object on success, false on failure
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getForm($data = [], $loadData = true)
|
||||
{
|
||||
// Get the form.
|
||||
$form = $this->loadForm('com_users.mail', 'mail', ['control' => 'jform', 'load_data' => $loadData]);
|
||||
|
||||
if (empty($form)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the data that should be injected in the form.
|
||||
*
|
||||
* @return mixed The data for the form.
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function loadFormData()
|
||||
{
|
||||
// Check the session for previously entered form data.
|
||||
$data = Factory::getApplication()->getUserState('com_users.display.mail.data', []);
|
||||
|
||||
$this->preprocessData('com_users.mail', $data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to preprocess the form
|
||||
*
|
||||
* @param Form $form A form object.
|
||||
* @param mixed $data The data expected for the form.
|
||||
* @param string $group The name of the plugin group to import (defaults to "content").
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception if there is an error loading the form.
|
||||
*/
|
||||
protected function preprocessForm(Form $form, $data, $group = 'user')
|
||||
{
|
||||
parent::preprocessForm($form, $data, $group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the email
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function send()
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$data = $app->getInput()->post->get('jform', [], 'array');
|
||||
$user = $this->getCurrentUser();
|
||||
$access = new Access();
|
||||
$db = $this->getDatabase();
|
||||
$language = Factory::getLanguage();
|
||||
|
||||
$mode = \array_key_exists('mode', $data) ? (int) $data['mode'] : 0;
|
||||
$subject = \array_key_exists('subject', $data) ? $data['subject'] : '';
|
||||
$grp = \array_key_exists('group', $data) ? (int) $data['group'] : 0;
|
||||
$recurse = \array_key_exists('recurse', $data) ? (int) $data['recurse'] : 0;
|
||||
$bcc = \array_key_exists('bcc', $data) ? (int) $data['bcc'] : 0;
|
||||
$disabled = \array_key_exists('disabled', $data) ? (int) $data['disabled'] : 0;
|
||||
$message_body = \array_key_exists('message', $data) ? $data['message'] : '';
|
||||
|
||||
// Automatically removes html formatting
|
||||
if (!$mode) {
|
||||
$message_body = InputFilter::getInstance()->clean($message_body, 'string');
|
||||
}
|
||||
|
||||
// Check for a message body and subject
|
||||
if (!$message_body || !$subject) {
|
||||
$app->setUserState('com_users.display.mail.data', $data);
|
||||
$this->setError(Text::_('COM_USERS_MAIL_PLEASE_FILL_IN_THE_FORM_CORRECTLY'));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get users in the group out of the ACL, if group is provided.
|
||||
$to = $grp !== 0 ? $access->getUsersByGroup($grp, $recurse) : [];
|
||||
|
||||
// When group is provided but no users are found in the group.
|
||||
if ($grp !== 0 && !$to) {
|
||||
$rows = [];
|
||||
} else {
|
||||
// Get all users email and group except for senders
|
||||
$uid = (int) $user->id;
|
||||
$query = $db->getQuery(true)
|
||||
->select(
|
||||
[
|
||||
$db->quoteName('email'),
|
||||
$db->quoteName('name'),
|
||||
]
|
||||
)
|
||||
->from($db->quoteName('#__users'))
|
||||
->where($db->quoteName('id') . ' != :id')
|
||||
->bind(':id', $uid, ParameterType::INTEGER);
|
||||
|
||||
if ($grp !== 0) {
|
||||
$query->whereIn($db->quoteName('id'), $to);
|
||||
}
|
||||
|
||||
if ($disabled === 0) {
|
||||
$query->where($db->quoteName('block') . ' = 0');
|
||||
}
|
||||
|
||||
$db->setQuery($query);
|
||||
$rows = $db->loadObjectList();
|
||||
}
|
||||
|
||||
// Check to see if there are any users in this group before we continue
|
||||
if (!$rows) {
|
||||
$app->setUserState('com_users.display.mail.data', $data);
|
||||
|
||||
if (\in_array($user->id, $to)) {
|
||||
$this->setError(Text::_('COM_USERS_MAIL_ONLY_YOU_COULD_BE_FOUND_IN_THIS_GROUP'));
|
||||
} else {
|
||||
$this->setError(Text::_('COM_USERS_MAIL_NO_USERS_COULD_BE_FOUND_IN_THIS_GROUP'));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the Mailer
|
||||
$mailer = new MailTemplate('com_users.massmail.mail', $language->getTag());
|
||||
$params = ComponentHelper::getParams('com_users');
|
||||
|
||||
try {
|
||||
// Build email message format.
|
||||
$data = [
|
||||
'subject' => stripslashes($subject),
|
||||
'body' => $message_body,
|
||||
'subjectprefix' => $params->get('mailSubjectPrefix', ''),
|
||||
'bodysuffix' => $params->get('mailBodySuffix', ''),
|
||||
];
|
||||
$mailer->addTemplateData($data);
|
||||
|
||||
$recipientType = $bcc ? 'bcc' : 'to';
|
||||
|
||||
// Add recipients
|
||||
foreach ($rows as $row) {
|
||||
$mailer->addRecipient($row->email, $row->name, $recipientType);
|
||||
}
|
||||
|
||||
if ($bcc) {
|
||||
$mailer->addRecipient($app->get('mailfrom'), $app->get('fromname'));
|
||||
}
|
||||
|
||||
// Send the Mail
|
||||
$rs = $mailer->send();
|
||||
} catch (MailDisabledException | phpMailerException $exception) {
|
||||
try {
|
||||
Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror');
|
||||
|
||||
$rs = false;
|
||||
} catch (\RuntimeException $exception) {
|
||||
Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning');
|
||||
|
||||
$rs = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for an error
|
||||
if ($rs !== true) {
|
||||
$app->setUserState('com_users.display.mail.data', $data);
|
||||
$this->setError($mailer->ErrorInfo);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($rs)) {
|
||||
$app->setUserState('com_users.display.mail.data', $data);
|
||||
$this->setError(Text::_('COM_USERS_MAIL_THE_MAIL_COULD_NOT_BE_SENT'));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the data (specially for the 'mode', 'group' and 'bcc': they could not exist in the array
|
||||
* when the box is not checked and in this case, the default value would be used instead of the '0'
|
||||
* one)
|
||||
*/
|
||||
$data['mode'] = $mode;
|
||||
$data['subject'] = $subject;
|
||||
$data['group'] = $grp;
|
||||
$data['recurse'] = $recurse;
|
||||
$data['bcc'] = $bcc;
|
||||
$data['message'] = $message_body;
|
||||
$app->setUserState('com_users.display.mail.data', []);
|
||||
$app->enqueueMessage(Text::plural('COM_USERS_MAIL_EMAIL_SENT_TO_N_USERS', \count($rows)), 'message');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
257
administrator/components/com_users/src/Model/MethodModel.php
Normal file
257
administrator/components/com_users/src/Model/MethodModel.php
Normal file
@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Event\MultiFactor\GetSetup;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
use Joomla\CMS\User\User;
|
||||
use Joomla\Component\Users\Administrator\DataShape\SetupRenderOptions;
|
||||
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
|
||||
use Joomla\Component\Users\Administrator\Table\MfaTable;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Multi-factor Authentication management model
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class MethodModel extends BaseDatabaseModel
|
||||
{
|
||||
/**
|
||||
* List of MFA Methods
|
||||
*
|
||||
* @var array
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $mfaMethods = null;
|
||||
|
||||
/**
|
||||
* Get the specified MFA Method's record
|
||||
*
|
||||
* @param string $method The Method to retrieve.
|
||||
*
|
||||
* @return array
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function getMethod(string $method): array
|
||||
{
|
||||
if (!$this->methodExists($method)) {
|
||||
return [
|
||||
'name' => $method,
|
||||
'display' => '',
|
||||
'shortinfo' => '',
|
||||
'image' => '',
|
||||
'canDisable' => true,
|
||||
'allowMultiple' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->mfaMethods[$method];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the specified MFA Method available?
|
||||
*
|
||||
* @param string $method The Method to check.
|
||||
*
|
||||
* @return boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function methodExists(string $method): bool
|
||||
{
|
||||
if (!\is_array($this->mfaMethods)) {
|
||||
$this->populateMfaMethods();
|
||||
}
|
||||
|
||||
return isset($this->mfaMethods[$method]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User|null $user The user record. Null to use the currently logged in user.
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function getRenderOptions(?User $user = null): SetupRenderOptions
|
||||
{
|
||||
if (\is_null($user)) {
|
||||
$user = $this->getCurrentUser();
|
||||
}
|
||||
|
||||
$renderOptions = new SetupRenderOptions();
|
||||
|
||||
$event = new GetSetup($this->getRecord($user));
|
||||
$results = Factory::getApplication()
|
||||
->getDispatcher()
|
||||
->dispatch($event->getName(), $event)
|
||||
->getArgument('result', []);
|
||||
|
||||
if (empty($results)) {
|
||||
return $renderOptions;
|
||||
}
|
||||
|
||||
foreach ($results as $result) {
|
||||
if (empty($result)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $renderOptions->merge($result);
|
||||
}
|
||||
|
||||
return $renderOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the specified MFA record. It will return a fake default record when no record ID is specified.
|
||||
*
|
||||
* @param User|null $user The user record. Null to use the currently logged in user.
|
||||
*
|
||||
* @return MfaTable
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function getRecord(User $user = null): MfaTable
|
||||
{
|
||||
if (\is_null($user)) {
|
||||
$user = $this->getCurrentUser();
|
||||
}
|
||||
|
||||
$defaultRecord = $this->getDefaultRecord($user);
|
||||
$id = (int) $this->getState('id', 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
return $defaultRecord;
|
||||
}
|
||||
|
||||
/** @var MfaTable $record */
|
||||
$record = $this->getTable('Mfa', 'Administrator');
|
||||
$loaded = $record->load(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'id' => $id,
|
||||
]
|
||||
);
|
||||
|
||||
if (!$loaded) {
|
||||
return $defaultRecord;
|
||||
}
|
||||
|
||||
if (!$this->methodExists($record->method)) {
|
||||
return $defaultRecord;
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the title to use for the page
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function getPageTitle(): string
|
||||
{
|
||||
$task = $this->getState('task', 'edit');
|
||||
|
||||
switch ($task) {
|
||||
case 'mfa':
|
||||
$key = 'COM_USERS_USER_MULTIFACTOR_AUTH';
|
||||
break;
|
||||
|
||||
default:
|
||||
$key = sprintf('COM_USERS_MFA_%s_PAGE_HEAD', $task);
|
||||
break;
|
||||
}
|
||||
|
||||
return Text::_($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User|null $user The user record. Null to use the current user.
|
||||
*
|
||||
* @return MfaTable
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected function getDefaultRecord(?User $user = null): MfaTable
|
||||
{
|
||||
if (\is_null($user)) {
|
||||
$user = $this->getCurrentUser();
|
||||
}
|
||||
|
||||
$method = $this->getState('method');
|
||||
$title = '';
|
||||
|
||||
if (\is_null($this->mfaMethods)) {
|
||||
$this->populateMfaMethods();
|
||||
}
|
||||
|
||||
if ($method && isset($this->mfaMethods[$method])) {
|
||||
$title = $this->mfaMethods[$method]['display'];
|
||||
}
|
||||
|
||||
/** @var MfaTable $record */
|
||||
$record = $this->getTable('Mfa', 'Administrator');
|
||||
|
||||
$record->bind(
|
||||
[
|
||||
'id' => null,
|
||||
'user_id' => $user->id,
|
||||
'title' => $title,
|
||||
'method' => $method,
|
||||
'default' => 0,
|
||||
'options' => [],
|
||||
]
|
||||
);
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the list of MFA Methods
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function populateMfaMethods(): void
|
||||
{
|
||||
$this->mfaMethods = [];
|
||||
$mfaMethods = MfaHelper::getMfaMethods();
|
||||
|
||||
if (empty($mfaMethods)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($mfaMethods as $method) {
|
||||
$this->mfaMethods[$method['name']] = $method;
|
||||
}
|
||||
|
||||
// We also need to add the backup codes Method
|
||||
$this->mfaMethods['backupcodes'] = [
|
||||
'name' => 'backupcodes',
|
||||
'display' => Text::_('COM_USERS_USER_BACKUPCODES'),
|
||||
'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'),
|
||||
'image' => 'media/com_users/images/emergency.svg',
|
||||
'canDisable' => false,
|
||||
'allowMultiple' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
218
administrator/components/com_users/src/Model/MethodsModel.php
Normal file
218
administrator/components/com_users/src/Model/MethodsModel.php
Normal file
@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Date\Date;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
use Joomla\CMS\User\User;
|
||||
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
|
||||
use Joomla\Database\ParameterType;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Multi-factor Authentication Methods list page's model
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class MethodsModel extends BaseDatabaseModel
|
||||
{
|
||||
/**
|
||||
* Returns a list of all available MFA methods and their currently active records for a given user.
|
||||
*
|
||||
* @param User|null $user The user object. Skip to use the current user.
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function getMethods(?User $user = null): array
|
||||
{
|
||||
if (\is_null($user)) {
|
||||
$user = $this->getCurrentUser();
|
||||
}
|
||||
|
||||
if ($user->guest) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get an associative array of MFA Methods
|
||||
$rawMethods = MfaHelper::getMfaMethods();
|
||||
$methods = [];
|
||||
|
||||
foreach ($rawMethods as $method) {
|
||||
$method['active'] = [];
|
||||
$methods[$method['name']] = $method;
|
||||
}
|
||||
|
||||
// Put the user MFA records into the Methods array
|
||||
$userMfaRecords = MfaHelper::getUserMfaRecords($user->id);
|
||||
|
||||
if (!empty($userMfaRecords)) {
|
||||
foreach ($userMfaRecords as $record) {
|
||||
if (!isset($methods[$record->method])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$methods[$record->method]->addActiveMethod($record);
|
||||
}
|
||||
}
|
||||
|
||||
return $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all Multi-factor Authentication Methods for the given user.
|
||||
*
|
||||
* @param User|null $user The user object to reset MFA for. Null to use the current user.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function deleteAll(?User $user = null): void
|
||||
{
|
||||
// Make sure we have a user object
|
||||
if (\is_null($user)) {
|
||||
$user = $this->getCurrentUser() ?: Factory::getApplication()->getIdentity();
|
||||
}
|
||||
|
||||
// If the user object is a guest (who can't have MFA) we stop with an error
|
||||
if ($user->guest) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true)
|
||||
->delete($db->quoteName('#__user_mfa'))
|
||||
->where($db->quoteName('user_id') . ' = :user_id')
|
||||
->bind(':user_id', $user->id, ParameterType::INTEGER);
|
||||
$db->setQuery($query)->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a relative timestamp. It deals with timestamps today and yesterday in a special manner. Example returns:
|
||||
* Yesterday, 13:12
|
||||
* Today, 08:33
|
||||
* January 1, 2015
|
||||
*
|
||||
* @param string $dateTimeText The database time string to use, e.g. "2017-01-13 13:25:36"
|
||||
*
|
||||
* @return string The formatted, human-readable date
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function formatRelative(?string $dateTimeText): string
|
||||
{
|
||||
if (empty($dateTimeText)) {
|
||||
return Text::_('JNEVER');
|
||||
}
|
||||
|
||||
// The timestamp is given in UTC. Make sure Joomla! parses it as such.
|
||||
$utcTimeZone = new \DateTimeZone('UTC');
|
||||
$jDate = new Date($dateTimeText, $utcTimeZone);
|
||||
$unixStamp = $jDate->toUnix();
|
||||
|
||||
// I'm pretty sure we didn't have MFA in Joomla back in 1970 ;)
|
||||
if ($unixStamp < 0) {
|
||||
return Text::_('JNEVER');
|
||||
}
|
||||
|
||||
// I need to display the date in the user's local timezone. That's how you do it.
|
||||
$user = $this->getCurrentUser();
|
||||
$userTZ = $user->getParam('timezone', 'UTC');
|
||||
$tz = new \DateTimeZone($userTZ);
|
||||
$jDate->setTimezone($tz);
|
||||
|
||||
// Default format string: way in the past, the time of the day is not important
|
||||
$formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_PAST');
|
||||
$containerString = Text::_('COM_USERS_MFA_LBL_PAST');
|
||||
|
||||
// If the timestamp is within the last 72 hours we may need a special format
|
||||
if ($unixStamp > (time() - (72 * 3600))) {
|
||||
// Is this timestamp today?
|
||||
$jNow = new Date();
|
||||
$jNow->setTimezone($tz);
|
||||
$checkNow = $jNow->format('Ymd', true);
|
||||
$checkDate = $jDate->format('Ymd', true);
|
||||
|
||||
if ($checkDate == $checkNow) {
|
||||
$formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_TODAY');
|
||||
$containerString = Text::_('COM_USERS_MFA_LBL_TODAY');
|
||||
} else {
|
||||
// Is this timestamp yesterday?
|
||||
$jYesterday = clone $jNow;
|
||||
$jYesterday->setTime(0, 0, 0);
|
||||
$oneSecond = new \DateInterval('PT1S');
|
||||
$jYesterday->sub($oneSecond);
|
||||
$checkYesterday = $jYesterday->format('Ymd', true);
|
||||
|
||||
if ($checkDate == $checkYesterday) {
|
||||
$formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_YESTERDAY');
|
||||
$containerString = Text::_('COM_USERS_MFA_LBL_YESTERDAY');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf($containerString, $jDate->format($formatString, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the user's "don't show this again" flag.
|
||||
*
|
||||
* @param User $user The user to check
|
||||
* @param bool $flag True to set the flag, false to unset it (it will be set to 0, actually)
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function setFlag(User $user, bool $flag = true): void
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$profileKey = 'mfa.dontshow';
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('profile_value'))
|
||||
->from($db->quoteName('#__user_profiles'))
|
||||
->where($db->quoteName('user_id') . ' = :user_id')
|
||||
->where($db->quoteName('profile_key') . ' = :profileKey')
|
||||
->bind(':user_id', $user->id, ParameterType::INTEGER)
|
||||
->bind(':profileKey', $profileKey, ParameterType::STRING);
|
||||
|
||||
try {
|
||||
$result = $db->setQuery($query)->loadResult();
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exists = !\is_null($result);
|
||||
|
||||
$object = (object) [
|
||||
'user_id' => $user->id,
|
||||
'profile_key' => 'mfa.dontshow',
|
||||
'profile_value' => ($flag ? 1 : 0),
|
||||
'ordering' => 1,
|
||||
];
|
||||
|
||||
if (!$exists) {
|
||||
$db->insertObject('#__user_profiles', $object);
|
||||
} else {
|
||||
$db->updateObject('#__user_profiles', $object, ['user_id', 'profile_key']);
|
||||
}
|
||||
}
|
||||
}
|
||||
140
administrator/components/com_users/src/Model/NoteModel.php
Normal file
140
administrator/components/com_users/src/Model/NoteModel.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Model\AdminModel;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\CMS\Versioning\VersionableModelTrait;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User note model.
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
class NoteModel extends AdminModel
|
||||
{
|
||||
use VersionableModelTrait;
|
||||
|
||||
/**
|
||||
* The type alias for this content type.
|
||||
*
|
||||
* @var string
|
||||
* @since 3.2
|
||||
*/
|
||||
public $typeAlias = 'com_users.note';
|
||||
|
||||
/**
|
||||
* Method to get the record form.
|
||||
*
|
||||
* @param array $data Data for the form.
|
||||
* @param boolean $loadData True if the form is to load its own data (default case), false if not.
|
||||
*
|
||||
* @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
public function getForm($data = [], $loadData = true)
|
||||
{
|
||||
// Get the form.
|
||||
$form = $this->loadForm('com_users.note', 'note', ['control' => 'jform', 'load_data' => $loadData]);
|
||||
|
||||
if (empty($form)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get a single record.
|
||||
*
|
||||
* @param integer $pk The id of the primary key.
|
||||
*
|
||||
* @return mixed Object on success, false on failure.
|
||||
*
|
||||
* @since 2.5
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getItem($pk = null)
|
||||
{
|
||||
$result = parent::getItem($pk);
|
||||
|
||||
// Get the dispatcher and load the content plugins.
|
||||
PluginHelper::importPlugin('content');
|
||||
|
||||
// Load the user plugins for backward compatibility (v3.3.3 and earlier).
|
||||
PluginHelper::importPlugin('user');
|
||||
|
||||
// Trigger the data preparation event.
|
||||
Factory::getApplication()->triggerEvent('onContentPrepareData', ['com_users.note', $result]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the data that should be injected in the form.
|
||||
*
|
||||
* @return mixed The data for the form.
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function loadFormData()
|
||||
{
|
||||
// Get the application
|
||||
$app = Factory::getApplication();
|
||||
|
||||
// Check the session for previously entered form data.
|
||||
$data = $app->getUserState('com_users.edit.note.data', []);
|
||||
|
||||
if (empty($data)) {
|
||||
$data = $this->getItem();
|
||||
|
||||
// Prime some default values.
|
||||
if ($this->getState('note.id') == 0) {
|
||||
$data->set('catid', $app->getInput()->get('catid', $app->getUserState('com_users.notes.filter.category_id'), 'int'));
|
||||
}
|
||||
|
||||
$userId = $app->getInput()->get('u_id', 0, 'int');
|
||||
|
||||
if ($userId != 0) {
|
||||
$data->user_id = $userId;
|
||||
}
|
||||
}
|
||||
|
||||
$this->preprocessData('com_users.note', $data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to auto-populate the model state.
|
||||
*
|
||||
* Note. Calling getState in this method will result in recursion.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 2.5
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function populateState()
|
||||
{
|
||||
parent::populateState();
|
||||
|
||||
$userId = Factory::getApplication()->getInput()->get('u_id', 0, 'int');
|
||||
$this->setState('note.user_id', $userId);
|
||||
}
|
||||
}
|
||||
231
administrator/components/com_users/src/Model/NotesModel.php
Normal file
231
administrator/components/com_users/src/Model/NotesModel.php
Normal file
@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\MVC\Model\ListModel;
|
||||
use Joomla\CMS\User\User;
|
||||
use Joomla\Database\DatabaseQuery;
|
||||
use Joomla\Database\ParameterType;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User notes model class.
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
class NotesModel extends ListModel
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $config An optional associative array of configuration settings.
|
||||
* @param MVCFactoryInterface $factory The factory.
|
||||
*
|
||||
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
|
||||
* @since 3.2
|
||||
*/
|
||||
public function __construct($config = [], MVCFactoryInterface $factory = null)
|
||||
{
|
||||
// Set the list ordering fields.
|
||||
if (empty($config['filter_fields'])) {
|
||||
$config['filter_fields'] = [
|
||||
'id', 'a.id',
|
||||
'user_id', 'a.user_id',
|
||||
'u.name',
|
||||
'subject', 'a.subject',
|
||||
'catid', 'a.catid', 'category_id',
|
||||
'state', 'a.state', 'published',
|
||||
'c.title',
|
||||
'review_time', 'a.review_time',
|
||||
'publish_up', 'a.publish_up',
|
||||
'publish_down', 'a.publish_down',
|
||||
'level', 'c.level',
|
||||
];
|
||||
}
|
||||
|
||||
parent::__construct($config, $factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an SQL query to load the list data.
|
||||
*
|
||||
* @return DatabaseQuery A DatabaseQuery object to retrieve the data set.
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
protected function getListQuery()
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true);
|
||||
|
||||
// Select the required fields from the table.
|
||||
$query->select(
|
||||
$this->getState(
|
||||
'list.select',
|
||||
'a.id, a.subject, a.checked_out, a.checked_out_time,' .
|
||||
'a.catid, a.created_time, a.review_time,' .
|
||||
'a.state, a.publish_up, a.publish_down'
|
||||
)
|
||||
);
|
||||
$query->from('#__user_notes AS a');
|
||||
|
||||
// Join over the category
|
||||
$query->select('c.title AS category_title, c.params AS category_params')
|
||||
->join('LEFT', '#__categories AS c ON c.id = a.catid');
|
||||
|
||||
// Join over the users for the note user.
|
||||
$query->select('u.name AS user_name')
|
||||
->join('LEFT', '#__users AS u ON u.id = a.user_id');
|
||||
|
||||
// Join over the users for the checked out user.
|
||||
$query->select('uc.name AS editor')
|
||||
->join('LEFT', '#__users AS uc ON uc.id = a.checked_out');
|
||||
|
||||
// Filter by search in title
|
||||
$search = $this->getState('filter.search');
|
||||
|
||||
if (!empty($search)) {
|
||||
if (stripos($search, 'id:') === 0) {
|
||||
$search3 = (int) substr($search, 3);
|
||||
$query->where($db->quoteName('a.id') . ' = :id');
|
||||
$query->bind(':id', $search3, ParameterType::INTEGER);
|
||||
} elseif (stripos($search, 'uid:') === 0) {
|
||||
$search4 = (int) substr($search, 4);
|
||||
$query->where($db->quoteName('a.user_id') . ' = :id');
|
||||
$query->bind(':id', $search4, ParameterType::INTEGER);
|
||||
} else {
|
||||
$search = '%' . trim($search) . '%';
|
||||
$query->where(
|
||||
'(' . $db->quoteName('a.subject') . ' LIKE :subject'
|
||||
. ' OR ' . $db->quoteName('u.name') . ' LIKE :name'
|
||||
. ' OR ' . $db->quoteName('u.username') . ' LIKE :username)'
|
||||
);
|
||||
$query->bind(':subject', $search);
|
||||
$query->bind(':name', $search);
|
||||
$query->bind(':username', $search);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by published state
|
||||
$published = $this->getState('filter.published');
|
||||
|
||||
if (is_numeric($published)) {
|
||||
$query->where($db->quoteName('a.state') . ' = :state')
|
||||
->bind(':state', $published, ParameterType::INTEGER);
|
||||
} elseif ($published !== '*') {
|
||||
$query->whereIn($db->quoteName('a.state'), [0, 1]);
|
||||
}
|
||||
|
||||
// Filter by a single category.
|
||||
$categoryId = (int) $this->getState('filter.category_id');
|
||||
|
||||
if ($categoryId) {
|
||||
$query->where($db->quoteName('a.catid') . ' = :catid')
|
||||
->bind(':catid', $categoryId, ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
// Filter by a single user.
|
||||
$userId = (int) $this->getState('filter.user_id');
|
||||
|
||||
if ($userId) {
|
||||
// Add the body and where filter.
|
||||
$query->select('a.body')
|
||||
->where($db->quoteName('a.user_id') . ' = :user_id')
|
||||
->bind(':user_id', $userId, ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
// Filter on the level.
|
||||
if ($level = $this->getState('filter.level')) {
|
||||
$level = (int) $level;
|
||||
$query->where($db->quoteName('c.level') . ' <= :level')
|
||||
->bind(':level', $level, ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
// Add the list ordering clause.
|
||||
$query->order($db->escape($this->getState('list.ordering', 'a.review_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC')));
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get a store id based on model configuration state.
|
||||
*
|
||||
* This is necessary because the model is used by the component and
|
||||
* different modules that might need different sets of data or different
|
||||
* ordering requirements.
|
||||
*
|
||||
* @param string $id A prefix for the store id.
|
||||
*
|
||||
* @return string A store id.
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
protected function getStoreId($id = '')
|
||||
{
|
||||
// Compile the store id.
|
||||
$id .= ':' . $this->getState('filter.search');
|
||||
$id .= ':' . $this->getState('filter.published');
|
||||
$id .= ':' . $this->getState('filter.category_id');
|
||||
$id .= ':' . $this->getState('filter.user_id');
|
||||
$id .= ':' . $this->getState('filter.level');
|
||||
|
||||
return parent::getStoreId($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a user object if the user filter is set.
|
||||
*
|
||||
* @return User The User object
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
// Filter by search in title
|
||||
$search = (int) $this->getState('filter.user_id');
|
||||
|
||||
if ($search != 0) {
|
||||
$user->load((int) $search);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to auto-populate the model state.
|
||||
*
|
||||
* Note. Calling getState in this method will result in recursion.
|
||||
*
|
||||
* @param string $ordering An optional ordering field.
|
||||
* @param string $direction An optional direction (asc|desc).
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function populateState($ordering = 'a.review_time', $direction = 'desc')
|
||||
{
|
||||
// Adjust the context to support modal layouts.
|
||||
if ($layout = Factory::getApplication()->getInput()->get('layout')) {
|
||||
$this->context .= '.' . $layout;
|
||||
}
|
||||
|
||||
parent::populateState($ordering, $direction);
|
||||
}
|
||||
}
|
||||
1079
administrator/components/com_users/src/Model/UserModel.php
Normal file
1079
administrator/components/com_users/src/Model/UserModel.php
Normal file
File diff suppressed because it is too large
Load Diff
607
administrator/components/com_users/src/Model/UsersModel.php
Normal file
607
administrator/components/com_users/src/Model/UsersModel.php
Normal file
@ -0,0 +1,607 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2008 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Model;
|
||||
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Date\Date;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Form\Form;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\MVC\Model\ListModel;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\Database\DatabaseQuery;
|
||||
use Joomla\Database\ParameterType;
|
||||
use Joomla\Utilities\ArrayHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Methods supporting a list of user records.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class UsersModel extends ListModel
|
||||
{
|
||||
/**
|
||||
* A list of filter variables to not merge into the model's state
|
||||
*
|
||||
* @var array
|
||||
* @since 4.0.0
|
||||
*/
|
||||
protected $filterForbiddenList = ['groups', 'excluded'];
|
||||
|
||||
/**
|
||||
* Override parent constructor.
|
||||
*
|
||||
* @param array $config An optional associative array of configuration settings.
|
||||
* @param MVCFactoryInterface $factory The factory.
|
||||
*
|
||||
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
|
||||
* @since 3.2
|
||||
*/
|
||||
public function __construct($config = [], MVCFactoryInterface $factory = null)
|
||||
{
|
||||
if (empty($config['filter_fields'])) {
|
||||
$config['filter_fields'] = [
|
||||
'id', 'a.id',
|
||||
'name', 'a.name',
|
||||
'username', 'a.username',
|
||||
'email', 'a.email',
|
||||
'block', 'a.block',
|
||||
'sendEmail', 'a.sendEmail',
|
||||
'registerDate', 'a.registerDate',
|
||||
'lastvisitDate', 'a.lastvisitDate',
|
||||
'activation', 'a.activation',
|
||||
'active',
|
||||
'group_id',
|
||||
'range',
|
||||
'lastvisitrange',
|
||||
'state',
|
||||
'mfa',
|
||||
];
|
||||
}
|
||||
|
||||
parent::__construct($config, $factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to auto-populate the model state.
|
||||
*
|
||||
* Note. Calling getState in this method will result in recursion.
|
||||
*
|
||||
* @param string $ordering An optional ordering field.
|
||||
* @param string $direction An optional direction (asc|desc).
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function populateState($ordering = 'a.name', $direction = 'asc')
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$input = $app->getInput();
|
||||
|
||||
// Adjust the context to support modal layouts.
|
||||
if ($layout = $input->get('layout', 'default', 'cmd')) {
|
||||
$this->context .= '.' . $layout;
|
||||
}
|
||||
|
||||
$groups = json_decode(base64_decode($input->get('groups', '', 'BASE64')));
|
||||
|
||||
if (isset($groups)) {
|
||||
$groups = ArrayHelper::toInteger($groups);
|
||||
}
|
||||
|
||||
$this->setState('filter.groups', $groups);
|
||||
|
||||
$excluded = json_decode(base64_decode($input->get('excluded', '', 'BASE64')));
|
||||
|
||||
if (isset($excluded)) {
|
||||
$excluded = ArrayHelper::toInteger($excluded);
|
||||
}
|
||||
|
||||
$this->setState('filter.excluded', $excluded);
|
||||
|
||||
// Load the parameters.
|
||||
$params = ComponentHelper::getParams('com_users');
|
||||
$this->setState('params', $params);
|
||||
|
||||
// List state information.
|
||||
parent::populateState($ordering, $direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get a store id based on model configuration state.
|
||||
*
|
||||
* This is necessary because the model is used by the component and
|
||||
* different modules that might need different sets of data or different
|
||||
* ordering requirements.
|
||||
*
|
||||
* @param string $id A prefix for the store id.
|
||||
*
|
||||
* @return string A store id.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function getStoreId($id = '')
|
||||
{
|
||||
// Compile the store id.
|
||||
$id .= ':' . $this->getState('filter.search');
|
||||
$id .= ':' . $this->getState('filter.active');
|
||||
$id .= ':' . $this->getState('filter.state');
|
||||
$id .= ':' . $this->getState('filter.group_id');
|
||||
$id .= ':' . $this->getState('filter.range');
|
||||
|
||||
if (PluginHelper::isEnabled('multifactorauth')) {
|
||||
$id .= ':' . $this->getState('filter.mfa');
|
||||
}
|
||||
|
||||
return parent::getStoreId($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of users and adds expensive joins to the result set.
|
||||
*
|
||||
* @return mixed An array of data items on success, false on failure.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getItems()
|
||||
{
|
||||
// Get a storage key.
|
||||
$store = $this->getStoreId();
|
||||
|
||||
// Try to load the data from internal storage.
|
||||
if (empty($this->cache[$store])) {
|
||||
$groups = $this->getState('filter.groups');
|
||||
$groupId = $this->getState('filter.group_id');
|
||||
|
||||
if (isset($groups) && (empty($groups) || $groupId && !\in_array($groupId, $groups))) {
|
||||
$items = [];
|
||||
} else {
|
||||
$items = parent::getItems();
|
||||
}
|
||||
|
||||
// Bail out on an error or empty list.
|
||||
if (empty($items)) {
|
||||
$this->cache[$store] = $items;
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
// Joining the groups with the main query is a performance hog.
|
||||
// Find the information only on the result set.
|
||||
|
||||
// First pass: get list of the user ids and reset the counts.
|
||||
$userIds = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$userIds[] = (int) $item->id;
|
||||
|
||||
$item->group_count = 0;
|
||||
$item->group_names = '';
|
||||
$item->note_count = 0;
|
||||
}
|
||||
|
||||
// Get the counts from the database only for the users in the list.
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true);
|
||||
|
||||
// Join over the group mapping table.
|
||||
$query->select('map.user_id, COUNT(map.group_id) AS group_count')
|
||||
->from('#__user_usergroup_map AS map')
|
||||
->whereIn($db->quoteName('map.user_id'), $userIds)
|
||||
->group('map.user_id')
|
||||
// Join over the user groups table.
|
||||
->join('LEFT', '#__usergroups AS g2 ON g2.id = map.group_id');
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
// Load the counts into an array indexed on the user id field.
|
||||
try {
|
||||
$userGroups = $db->loadObjectList('user_id');
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$query->clear()
|
||||
->select('n.user_id, COUNT(n.id) As note_count')
|
||||
->from('#__user_notes AS n')
|
||||
->whereIn($db->quoteName('n.user_id'), $userIds)
|
||||
->where('n.state >= 0')
|
||||
->group('n.user_id');
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
// Load the counts into an array indexed on the aro.value field (the user id).
|
||||
try {
|
||||
$userNotes = $db->loadObjectList('user_id');
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Second pass: collect the group counts into the main items array.
|
||||
foreach ($items as &$item) {
|
||||
if (isset($userGroups[$item->id])) {
|
||||
$item->group_count = $userGroups[$item->id]->group_count;
|
||||
|
||||
// Group_concat in other databases is not supported
|
||||
$item->group_names = $this->getUserDisplayedGroups($item->id);
|
||||
}
|
||||
|
||||
if (isset($userNotes[$item->id])) {
|
||||
$item->note_count = $userNotes[$item->id]->note_count;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the items to the internal cache.
|
||||
$this->cache[$store] = $items;
|
||||
}
|
||||
|
||||
return $this->cache[$store];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filter form
|
||||
*
|
||||
* @param array $data data
|
||||
* @param boolean $loadData load current data
|
||||
*
|
||||
* @return Form|null The \JForm object or null if the form can't be found
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function getFilterForm($data = [], $loadData = true)
|
||||
{
|
||||
$form = parent::getFilterForm($data, $loadData);
|
||||
|
||||
if ($form && !PluginHelper::isEnabled('multifactorauth')) {
|
||||
$form->removeField('mfa', 'filter');
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build an SQL query to load the list data.
|
||||
*
|
||||
* @return DatabaseQuery
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function getListQuery()
|
||||
{
|
||||
// Create a new query object.
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true);
|
||||
|
||||
// Select the required fields from the table.
|
||||
$query->select(
|
||||
$this->getState(
|
||||
'list.select',
|
||||
'a.*'
|
||||
)
|
||||
);
|
||||
|
||||
$query->from($db->quoteName('#__users') . ' AS a');
|
||||
|
||||
// Include MFA information
|
||||
if (PluginHelper::isEnabled('multifactorauth')) {
|
||||
$subQuery = $db->getQuery(true)
|
||||
->select(
|
||||
[
|
||||
'MIN(' . $db->quoteName('user_id') . ') AS ' . $db->quoteName('uid'),
|
||||
'COUNT(*) AS ' . $db->quoteName('mfaRecords'),
|
||||
]
|
||||
)
|
||||
->from($db->quoteName('#__user_mfa'))
|
||||
->group($db->quoteName('user_id'));
|
||||
$query->select($db->quoteName('mfa.mfaRecords'))
|
||||
->join(
|
||||
'left',
|
||||
'(' . $subQuery . ') AS ' . $db->quoteName('mfa'),
|
||||
$db->quoteName('mfa.uid') . ' = ' . $db->quoteName('a.id')
|
||||
);
|
||||
|
||||
$mfaState = $this->getState('filter.mfa');
|
||||
|
||||
if (is_numeric($mfaState)) {
|
||||
$mfaState = (int) $mfaState;
|
||||
|
||||
if ($mfaState === 1) {
|
||||
$query->where(
|
||||
'((' . $db->quoteName('mfa.mfaRecords') . ' > 0) OR (' .
|
||||
$db->quoteName('a.otpKey') . ' IS NOT NULL AND ' .
|
||||
$db->quoteName('a.otpKey') . ' != ' . $db->quote('') . '))'
|
||||
);
|
||||
} else {
|
||||
$query->where(
|
||||
'((' . $db->quoteName('mfa.mfaRecords') . ' = 0 OR ' .
|
||||
$db->quoteName('mfa.mfaRecords') . ' IS NULL) AND (' .
|
||||
$db->quoteName('a.otpKey') . ' IS NULL OR ' .
|
||||
$db->quoteName('a.otpKey') . ' = ' . $db->quote('') . '))'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the model is set to check item state, add to the query.
|
||||
$state = $this->getState('filter.state');
|
||||
|
||||
if (is_numeric($state)) {
|
||||
$query->where($db->quoteName('a.block') . ' = :state')
|
||||
->bind(':state', $state, ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
// If the model is set to check the activated state, add to the query.
|
||||
$active = $this->getState('filter.active');
|
||||
|
||||
if (is_numeric($active)) {
|
||||
if ($active == '0') {
|
||||
$query->whereIn($db->quoteName('a.activation'), ['', '0']);
|
||||
} elseif ($active == '1') {
|
||||
$query->where($query->length($db->quoteName('a.activation')) . ' > 1');
|
||||
}
|
||||
}
|
||||
|
||||
// Filter the items over the group id if set.
|
||||
$groupId = $this->getState('filter.group_id');
|
||||
$groups = $this->getState('filter.groups');
|
||||
|
||||
if ($groupId || isset($groups)) {
|
||||
$group_by = [
|
||||
'a.id',
|
||||
'a.name',
|
||||
'a.username',
|
||||
'a.password',
|
||||
'a.block',
|
||||
'a.sendEmail',
|
||||
'a.registerDate',
|
||||
'a.lastvisitDate',
|
||||
'a.activation',
|
||||
'a.params',
|
||||
'a.email',
|
||||
'a.lastResetTime',
|
||||
'a.resetCount',
|
||||
'a.otpKey',
|
||||
'a.otep',
|
||||
'a.requireReset',
|
||||
];
|
||||
|
||||
if (PluginHelper::isEnabled('multifactorauth')) {
|
||||
$group_by[] = 'mfa.mfaRecords';
|
||||
}
|
||||
|
||||
$query->join('LEFT', '#__user_usergroup_map AS map2 ON map2.user_id = a.id')
|
||||
->group($db->quoteName($group_by));
|
||||
|
||||
if ($groupId) {
|
||||
$groupId = (int) $groupId;
|
||||
$query->where($db->quoteName('map2.group_id') . ' = :group_id')
|
||||
->bind(':group_id', $groupId, ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
if (isset($groups)) {
|
||||
$query->whereIn($db->quoteName('map2.group_id'), $groups);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter the items over the search string if set.
|
||||
$search = $this->getState('filter.search');
|
||||
|
||||
if (!empty($search)) {
|
||||
if (stripos($search, 'id:') === 0) {
|
||||
$ids = (int) substr($search, 3);
|
||||
$query->where($db->quoteName('a.id') . ' = :id');
|
||||
$query->bind(':id', $ids, ParameterType::INTEGER);
|
||||
} elseif (stripos($search, 'username:') === 0) {
|
||||
$search = '%' . substr($search, 9) . '%';
|
||||
$query->where($db->quoteName('a.username') . ' LIKE :username');
|
||||
$query->bind(':username', $search);
|
||||
} else {
|
||||
$search = '%' . trim($search) . '%';
|
||||
|
||||
// Add the clauses to the query.
|
||||
$query->where(
|
||||
'(' . $db->quoteName('a.name') . ' LIKE :name'
|
||||
. ' OR ' . $db->quoteName('a.username') . ' LIKE :username'
|
||||
. ' OR ' . $db->quoteName('a.email') . ' LIKE :email)'
|
||||
)
|
||||
->bind(':name', $search)
|
||||
->bind(':username', $search)
|
||||
->bind(':email', $search);
|
||||
}
|
||||
}
|
||||
|
||||
// Add filter for registration time ranges select list. UI Visitors get a range of predefined
|
||||
// values. API users can do a full range based on ISO8601
|
||||
$range = $this->getState('filter.range');
|
||||
$registrationStart = $this->getState('filter.registrationDateStart');
|
||||
$registrationEnd = $this->getState('filter.registrationDateEnd');
|
||||
|
||||
// Apply the range filter.
|
||||
if ($range || ($registrationStart && $registrationEnd)) {
|
||||
if ($range) {
|
||||
$dates = $this->buildDateRange($range);
|
||||
} else {
|
||||
$dates = [
|
||||
'dNow' => $registrationEnd,
|
||||
'dStart' => $registrationStart,
|
||||
];
|
||||
}
|
||||
|
||||
if ($dates['dStart'] !== false) {
|
||||
$dStart = $dates['dStart']->format('Y-m-d H:i:s');
|
||||
|
||||
if ($dates['dNow'] === false) {
|
||||
$query->where($db->quoteName('a.registerDate') . ' < :registerDate');
|
||||
$query->bind(':registerDate', $dStart);
|
||||
} else {
|
||||
$dNow = $dates['dNow']->format('Y-m-d H:i:s');
|
||||
|
||||
$query->where($db->quoteName('a.registerDate') . ' BETWEEN :registerDate1 AND :registerDate2');
|
||||
$query->bind(':registerDate1', $dStart);
|
||||
$query->bind(':registerDate2', $dNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add filter for last visit time ranges select list. UI Visitors get a range of predefined
|
||||
// values. API users can do a full range based on ISO8601
|
||||
$lastvisitrange = $this->getState('filter.lastvisitrange');
|
||||
$lastVisitStart = $this->getState('filter.lastVisitStart');
|
||||
$lastVisitEnd = $this->getState('filter.lastVisitEnd');
|
||||
|
||||
// Apply the range filter.
|
||||
if ($lastvisitrange || ($lastVisitStart && $lastVisitEnd)) {
|
||||
if ($lastvisitrange) {
|
||||
$dates = $this->buildDateRange($lastvisitrange);
|
||||
} else {
|
||||
$dates = [
|
||||
'dNow' => $lastVisitEnd,
|
||||
'dStart' => $lastVisitStart,
|
||||
];
|
||||
}
|
||||
|
||||
if ($dates['dStart'] === false) {
|
||||
$query->where($db->quoteName('a.lastvisitDate') . ' IS NULL');
|
||||
} else {
|
||||
$query->where($db->quoteName('a.lastvisitDate') . ' IS NOT NULL');
|
||||
|
||||
$dStart = $dates['dStart']->format('Y-m-d H:i:s');
|
||||
|
||||
if ($dates['dNow'] === false) {
|
||||
$query->where($db->quoteName('a.lastvisitDate') . ' < :lastvisitDate');
|
||||
$query->bind(':lastvisitDate', $dStart);
|
||||
} else {
|
||||
$dNow = $dates['dNow']->format('Y-m-d H:i:s');
|
||||
|
||||
$query->where($db->quoteName('a.lastvisitDate') . ' BETWEEN :lastvisitDate1 AND :lastvisitDate2');
|
||||
$query->bind(':lastvisitDate1', $dStart);
|
||||
$query->bind(':lastvisitDate2', $dNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by excluded users
|
||||
$excluded = $this->getState('filter.excluded');
|
||||
|
||||
if (!empty($excluded)) {
|
||||
$query->whereNotIn($db->quoteName('id'), $excluded);
|
||||
}
|
||||
|
||||
// Add the list ordering clause.
|
||||
$query->order(
|
||||
$db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))
|
||||
);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the date range to filter on.
|
||||
*
|
||||
* @param string $range The textual range to construct the filter for.
|
||||
*
|
||||
* @return array The date range to filter on.
|
||||
*
|
||||
* @since 3.6.0
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function buildDateRange($range)
|
||||
{
|
||||
// Get UTC for now.
|
||||
$dNow = new Date();
|
||||
$dStart = clone $dNow;
|
||||
|
||||
switch ($range) {
|
||||
case 'past_week':
|
||||
$dStart->modify('-7 day');
|
||||
break;
|
||||
|
||||
case 'past_1month':
|
||||
$dStart->modify('-1 month');
|
||||
break;
|
||||
|
||||
case 'past_3month':
|
||||
$dStart->modify('-3 month');
|
||||
break;
|
||||
|
||||
case 'past_6month':
|
||||
$dStart->modify('-6 month');
|
||||
$arr = [];
|
||||
break;
|
||||
|
||||
case 'post_year':
|
||||
$dNow = false;
|
||||
|
||||
// No break
|
||||
|
||||
case 'past_year':
|
||||
$dStart->modify('-1 year');
|
||||
break;
|
||||
|
||||
case 'today':
|
||||
// Ranges that need to align with local 'days' need special treatment.
|
||||
$app = Factory::getApplication();
|
||||
$offset = $app->get('offset');
|
||||
|
||||
// Reset the start time to be the beginning of today, local time.
|
||||
$dStart = new Date('now', $offset);
|
||||
$dStart->setTime(0, 0, 0);
|
||||
|
||||
// Now change the timezone back to UTC.
|
||||
$tz = new \DateTimeZone('GMT');
|
||||
$dStart->setTimezone($tz);
|
||||
break;
|
||||
case 'never':
|
||||
$dNow = false;
|
||||
$dStart = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return ['dNow' => $dNow, 'dStart' => $dStart];
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL server change
|
||||
*
|
||||
* @param integer $userId User identifier
|
||||
*
|
||||
* @return string Groups titles imploded :$
|
||||
*/
|
||||
protected function getUserDisplayedGroups($userId)
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('title'))
|
||||
->from($db->quoteName('#__usergroups', 'ug'))
|
||||
->join('LEFT', $db->quoteName('#__user_usergroup_map', 'map') . ' ON (ug.id = map.group_id)')
|
||||
->where($db->quoteName('map.user_id') . ' = :user_id')
|
||||
->bind(':user_id', $userId, ParameterType::INTEGER);
|
||||
|
||||
try {
|
||||
$result = $db->setQuery($query)->loadColumn();
|
||||
} catch (\RuntimeException $e) {
|
||||
$result = [];
|
||||
}
|
||||
|
||||
return implode("\n", $result);
|
||||
}
|
||||
}
|
||||
132
administrator/components/com_users/src/Service/Encrypt.php
Normal file
132
administrator/components/com_users/src/Service/Encrypt.php
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Service;
|
||||
|
||||
use Joomla\CMS\Encrypt\Aes;
|
||||
use Joomla\CMS\Factory;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Data encryption service.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class Encrypt
|
||||
{
|
||||
/**
|
||||
* The encryption engine used by this service
|
||||
*
|
||||
* @var Aes
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private $aes;
|
||||
|
||||
/**
|
||||
* EncryptService constructor.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the plaintext $data and return the ciphertext prefixed by ###AES128###
|
||||
*
|
||||
* @param string $data The plaintext data
|
||||
*
|
||||
* @return string The ciphertext, prefixed by ###AES128###
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function encrypt(string $data): string
|
||||
{
|
||||
if (!\is_object($this->aes)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$this->aes->setPassword($this->getPassword(), false);
|
||||
$encrypted = $this->aes->encryptString($data, true);
|
||||
|
||||
return '###AES128###' . $encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the ciphertext, prefixed by ###AES128###, and return the plaintext.
|
||||
*
|
||||
* @param string $data The ciphertext, prefixed by ###AES128###
|
||||
* @param bool $legacy Use legacy key expansion. We recommend against using it.
|
||||
*
|
||||
* @return string The plaintext data
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function decrypt(string $data, bool $legacy = false): string
|
||||
{
|
||||
if (substr($data, 0, 12) != '###AES128###') {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$data = substr($data, 12);
|
||||
|
||||
if (!\is_object($this->aes)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$this->aes->setPassword($this->getPassword(), $legacy);
|
||||
$decrypted = $this->aes->decryptString($data, true);
|
||||
|
||||
// Decrypted data is null byte padded. We have to remove the padding before proceeding.
|
||||
return rtrim($decrypted, "\0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the AES cryptography object
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function initialize(): void
|
||||
{
|
||||
if (\is_object($this->aes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$password = $this->getPassword();
|
||||
|
||||
if (empty($password)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->aes = new Aes('cbc');
|
||||
$this->aes->setPassword($password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the password used to encrypt information in the component
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function getPassword(): string
|
||||
{
|
||||
try {
|
||||
return Factory::getApplication()->get('secret', '');
|
||||
} catch (\Exception $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
425
administrator/components/com_users/src/Service/HTML/Users.php
Normal file
425
administrator/components/com_users/src/Service/HTML/Users.php
Normal file
@ -0,0 +1,425 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Service\HTML;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Language\LanguageHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
use Joomla\Database\ParameterType;
|
||||
use Joomla\Filesystem\Path;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Extended Utility class for the Users component.
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
class Users
|
||||
{
|
||||
/**
|
||||
* Display an image.
|
||||
*
|
||||
* @param string $src The source of the image
|
||||
*
|
||||
* @return string A <img> element if the specified file exists, otherwise, a null string
|
||||
*
|
||||
* @since 2.5
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function image($src)
|
||||
{
|
||||
$src = preg_replace('#[^A-Z0-9\-_\./]#i', '', $src);
|
||||
$file = JPATH_SITE . '/' . $src;
|
||||
|
||||
Path::check($file);
|
||||
|
||||
if (!file_exists($file)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '<img src="' . Uri::root() . $src . '" alt="">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an icon to add a note for this user.
|
||||
*
|
||||
* @param integer $userId The user ID
|
||||
*
|
||||
* @return string A link to add a note
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
public function addNote($userId)
|
||||
{
|
||||
$title = Text::_('COM_USERS_ADD_NOTE');
|
||||
|
||||
return '<a href="' . Route::_('index.php?option=com_users&task=note.add&u_id=' . (int) $userId)
|
||||
. '" class="btn btn-secondary btn-sm"><span class="icon-plus pe-1" aria-hidden="true">'
|
||||
. '</span>' . $title . '</a>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an icon to filter the notes list on this user.
|
||||
*
|
||||
* @param integer $count The number of notes for the user
|
||||
* @param integer $userId The user ID
|
||||
*
|
||||
* @return string A link to apply a filter
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
public function filterNotes($count, $userId)
|
||||
{
|
||||
if (empty($count)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$title = Text::_('COM_USERS_FILTER_NOTES');
|
||||
|
||||
return '<a href="' . Route::_('index.php?option=com_users&view=notes&filter[search]=uid:' . (int) $userId)
|
||||
. '" class="dropdown-item"><span class="icon-list pe-1" aria-hidden="true"></span>' . $title . '</a>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a note icon.
|
||||
*
|
||||
* @param integer $count The number of notes for the user
|
||||
* @param integer $userId The user ID
|
||||
*
|
||||
* @return string A link to a modal window with the user notes
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
public function notes($count, $userId)
|
||||
{
|
||||
if (empty($count)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$title = Text::plural('COM_USERS_N_USER_NOTES', $count);
|
||||
|
||||
return '<button type="button" data-bs-target="#userModal_' . (int) $userId . '" id="modal-' . (int) $userId
|
||||
. '" data-bs-toggle="modal" class="dropdown-item"><span class="icon-eye pe-1" aria-hidden="true"></span>' . $title . '</button>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the modal html.
|
||||
*
|
||||
* @param integer $count The number of notes for the user
|
||||
* @param integer $userId The user ID
|
||||
*
|
||||
* @return string The html for the rendered modal
|
||||
*
|
||||
* @since 3.4.1
|
||||
*/
|
||||
public function notesModal($count, $userId)
|
||||
{
|
||||
if (empty($count)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$title = Text::plural('COM_USERS_N_USER_NOTES', $count);
|
||||
$footer = '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">'
|
||||
. Text::_('JTOOLBAR_CLOSE') . '</button>';
|
||||
|
||||
return HTMLHelper::_(
|
||||
'bootstrap.renderModal',
|
||||
'userModal_' . (int) $userId,
|
||||
[
|
||||
'title' => $title,
|
||||
'backdrop' => 'static',
|
||||
'keyboard' => true,
|
||||
'closeButton' => true,
|
||||
'footer' => $footer,
|
||||
'url' => Route::_('index.php?option=com_users&view=notes&tmpl=component&layout=modal&filter[user_id]=' . (int) $userId),
|
||||
'height' => '300px',
|
||||
'width' => '800px',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an array of block/unblock user states to be used by jgrid.state,
|
||||
* State options will be different for any user
|
||||
* and for currently logged in user
|
||||
*
|
||||
* @param boolean $self True if state array is for currently logged in user
|
||||
*
|
||||
* @return array a list of possible states to display
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public function blockStates($self = false)
|
||||
{
|
||||
if ($self) {
|
||||
$states = [
|
||||
1 => [
|
||||
'task' => 'unblock',
|
||||
'text' => '',
|
||||
'active_title' => 'COM_USERS_TOOLBAR_BLOCK',
|
||||
'inactive_title' => '',
|
||||
'tip' => true,
|
||||
'active_class' => 'unpublish',
|
||||
'inactive_class' => 'unpublish',
|
||||
],
|
||||
0 => [
|
||||
'task' => 'block',
|
||||
'text' => '',
|
||||
'active_title' => '',
|
||||
'inactive_title' => 'COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF',
|
||||
'tip' => true,
|
||||
'active_class' => 'publish',
|
||||
'inactive_class' => 'publish',
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$states = [
|
||||
1 => [
|
||||
'task' => 'unblock',
|
||||
'text' => '',
|
||||
'active_title' => 'COM_USERS_TOOLBAR_UNBLOCK',
|
||||
'inactive_title' => '',
|
||||
'tip' => true,
|
||||
'active_class' => 'unpublish',
|
||||
'inactive_class' => 'unpublish',
|
||||
],
|
||||
0 => [
|
||||
'task' => 'block',
|
||||
'text' => '',
|
||||
'active_title' => 'COM_USERS_TOOLBAR_BLOCK',
|
||||
'inactive_title' => '',
|
||||
'tip' => true,
|
||||
'active_class' => 'publish',
|
||||
'inactive_class' => 'publish',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $states;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an array of activate states to be used by jgrid.state,
|
||||
*
|
||||
* @return array a list of possible states to display
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public function activateStates()
|
||||
{
|
||||
$states = [
|
||||
1 => [
|
||||
'task' => 'activate',
|
||||
'text' => '',
|
||||
'active_title' => 'COM_USERS_TOOLBAR_ACTIVATE',
|
||||
'inactive_title' => '',
|
||||
'tip' => true,
|
||||
'active_class' => 'unpublish',
|
||||
'inactive_class' => 'unpublish',
|
||||
],
|
||||
0 => [
|
||||
'task' => '',
|
||||
'text' => '',
|
||||
'active_title' => '',
|
||||
'inactive_title' => 'COM_USERS_ACTIVATED',
|
||||
'tip' => true,
|
||||
'active_class' => 'publish',
|
||||
'inactive_class' => 'publish',
|
||||
],
|
||||
];
|
||||
|
||||
return $states;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sanitized value
|
||||
*
|
||||
* @param mixed $value Value of the field
|
||||
*
|
||||
* @return mixed String/void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function value($value)
|
||||
{
|
||||
if (\is_string($value)) {
|
||||
$value = trim($value);
|
||||
}
|
||||
|
||||
if (empty($value)) {
|
||||
return Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND');
|
||||
}
|
||||
|
||||
if (!\is_array($value)) {
|
||||
return htmlspecialchars($value, ENT_COMPAT, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the space symbol
|
||||
*
|
||||
* @param mixed $value Value of the field
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function spacer($value)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sanitized template style
|
||||
*
|
||||
* @param mixed $value Value of the field
|
||||
*
|
||||
* @return mixed String/void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function templatestyle($value)
|
||||
{
|
||||
if (empty($value)) {
|
||||
return static::value($value);
|
||||
}
|
||||
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('title'))
|
||||
->from($db->quoteName('#__template_styles'))
|
||||
->where($db->quoteName('id') . ' = :id')
|
||||
->bind(':id', $value, ParameterType::INTEGER);
|
||||
$db->setQuery($query);
|
||||
$title = $db->loadResult();
|
||||
|
||||
if ($title) {
|
||||
return htmlspecialchars($title, ENT_COMPAT, 'UTF-8');
|
||||
}
|
||||
|
||||
return static::value('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sanitized language
|
||||
*
|
||||
* @param mixed $value Value of the field
|
||||
*
|
||||
* @return mixed String/void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function admin_language($value)
|
||||
{
|
||||
if (!$value) {
|
||||
return static::value($value);
|
||||
}
|
||||
|
||||
$path = LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR, $value);
|
||||
$file = $path . '/langmetadata.xml';
|
||||
|
||||
if (!is_file($file)) {
|
||||
// For language packs from before 4.0.
|
||||
$file = $path . '/' . $value . '.xml';
|
||||
|
||||
if (!is_file($file)) {
|
||||
return static::value($value);
|
||||
}
|
||||
}
|
||||
|
||||
$result = LanguageHelper::parseXMLLanguageFile($file);
|
||||
|
||||
if ($result) {
|
||||
return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8');
|
||||
}
|
||||
|
||||
return static::value($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sanitized language
|
||||
*
|
||||
* @param mixed $value Value of the field
|
||||
*
|
||||
* @return mixed String/void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function language($value)
|
||||
{
|
||||
if (!$value) {
|
||||
return static::value($value);
|
||||
}
|
||||
|
||||
$path = LanguageHelper::getLanguagePath(JPATH_SITE, $value);
|
||||
$file = $path . '/langmetadata.xml';
|
||||
|
||||
if (!is_file($file)) {
|
||||
// For language packs from before 4.0.
|
||||
$file = $path . '/' . $value . '.xml';
|
||||
|
||||
if (!is_file($file)) {
|
||||
return static::value($value);
|
||||
}
|
||||
}
|
||||
|
||||
$result = LanguageHelper::parseXMLLanguageFile($file);
|
||||
|
||||
if ($result) {
|
||||
return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8');
|
||||
}
|
||||
|
||||
return static::value($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sanitized editor name
|
||||
*
|
||||
* @param mixed $value Value of the field
|
||||
*
|
||||
* @return mixed String/void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
public function editor($value)
|
||||
{
|
||||
if (empty($value)) {
|
||||
return static::value($value);
|
||||
}
|
||||
|
||||
$db = Factory::getDbo();
|
||||
$lang = Factory::getLanguage();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('name'))
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('element') . ' = :element')
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('editors'))
|
||||
->bind(':element', $value);
|
||||
$db->setQuery($query);
|
||||
$title = $db->loadResult();
|
||||
|
||||
if ($title) {
|
||||
$lang->load("plg_editors_$value.sys", JPATH_ADMINISTRATOR)
|
||||
|| $lang->load("plg_editors_$value.sys", JPATH_PLUGINS . '/editors/' . $value);
|
||||
$lang->load($title . '.sys');
|
||||
|
||||
return Text::_($title);
|
||||
}
|
||||
|
||||
return static::value('');
|
||||
}
|
||||
}
|
||||
427
administrator/components/com_users/src/Table/MfaTable.php
Normal file
427
administrator/components/com_users/src/Table/MfaTable.php
Normal file
@ -0,0 +1,427 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Table;
|
||||
|
||||
use Joomla\CMS\Date\Date;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\CMS\Table\Table;
|
||||
use Joomla\CMS\User\CurrentUserInterface;
|
||||
use Joomla\CMS\User\CurrentUserTrait;
|
||||
use Joomla\CMS\User\UserFactoryAwareInterface;
|
||||
use Joomla\CMS\User\UserFactoryAwareTrait;
|
||||
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
|
||||
use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
|
||||
use Joomla\Component\Users\Administrator\Service\Encrypt;
|
||||
use Joomla\Database\DatabaseDriver;
|
||||
use Joomla\Database\ParameterType;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Table for the Multi-Factor Authentication records
|
||||
*
|
||||
* @property int $id Record ID.
|
||||
* @property int $user_id User ID
|
||||
* @property string $title Record title.
|
||||
* @property string $method MFA Method (corresponds to one of the plugins).
|
||||
* @property int $default Is this the default Method?
|
||||
* @property array $options Configuration options for the MFA Method.
|
||||
* @property string $created_on Date and time the record was created.
|
||||
* @property string $last_used Date and time the record was last used successfully.
|
||||
* @property int $tries Counter for unsuccessful tries
|
||||
* @property string $last_try Date and time of the last unsuccessful try
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class MfaTable extends Table implements CurrentUserInterface, UserFactoryAwareInterface
|
||||
{
|
||||
use CurrentUserTrait;
|
||||
use UserFactoryAwareTrait;
|
||||
|
||||
/**
|
||||
* Delete flags per ID, set up onBeforeDelete and used onAfterDelete
|
||||
*
|
||||
* @var array
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private $deleteFlags = [];
|
||||
|
||||
/**
|
||||
* Encryption service
|
||||
*
|
||||
* @var Encrypt
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private $encryptService;
|
||||
|
||||
/**
|
||||
* Indicates that columns fully support the NULL value in the database
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
// phpcs:ignore
|
||||
protected $_supportNullValue = true;
|
||||
|
||||
/**
|
||||
* Table constructor
|
||||
*
|
||||
* @param DatabaseDriver $db Database driver object
|
||||
* @param ?DispatcherInterface $dispatcher Events dispatcher object
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function __construct(DatabaseDriver $db, DispatcherInterface $dispatcher = null)
|
||||
{
|
||||
parent::__construct('#__user_mfa', 'id', $db, $dispatcher);
|
||||
|
||||
$this->encryptService = new Encrypt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to store a row in the database from the Table instance properties.
|
||||
*
|
||||
* If a primary key value is set the row with that primary key value will be updated with the instance property values.
|
||||
* If no primary key value is set a new row will be inserted into the database with the properties from the Table instance.
|
||||
*
|
||||
* @param boolean $updateNulls True to update fields even if they are null.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function store($updateNulls = true)
|
||||
{
|
||||
// Encrypt the options before saving them
|
||||
$this->options = $this->encryptService->encrypt(json_encode($this->options ?: []));
|
||||
|
||||
// Set last_used date to null if empty or zero date
|
||||
if (!((int) $this->last_used)) {
|
||||
$this->last_used = null;
|
||||
}
|
||||
|
||||
$records = MfaHelper::getUserMfaRecords($this->user_id);
|
||||
|
||||
if ($this->id) {
|
||||
// Existing record. Remove it from the list of records.
|
||||
$records = array_filter(
|
||||
$records,
|
||||
function ($rec) {
|
||||
return $rec->id != $this->id;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Update the dates on a new record
|
||||
if (empty($this->id)) {
|
||||
$this->created_on = Date::getInstance()->toSql();
|
||||
$this->last_used = null;
|
||||
}
|
||||
|
||||
// Do I need to mark this record as the default?
|
||||
if ($this->default == 0) {
|
||||
$hasDefaultRecord = array_reduce(
|
||||
$records,
|
||||
function ($carry, $record) {
|
||||
return $carry || ($record->default == 1);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
$this->default = $hasDefaultRecord ? 0 : 1;
|
||||
}
|
||||
|
||||
// Let's find out if we are saving a new MFA method record without having backup codes yet.
|
||||
$mustCreateBackupCodes = false;
|
||||
|
||||
if (empty($this->id) && $this->method !== 'backupcodes') {
|
||||
// Do I have any backup records?
|
||||
$hasBackupCodes = array_reduce(
|
||||
$records,
|
||||
function (bool $carry, $record) {
|
||||
return $carry || $record->method === 'backupcodes';
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
$mustCreateBackupCodes = !$hasBackupCodes;
|
||||
|
||||
// If the only other entry is the backup records one I need to make this the default method
|
||||
if ($hasBackupCodes && \count($records) === 1) {
|
||||
$this->default = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Store the record
|
||||
try {
|
||||
$result = parent::store($updateNulls);
|
||||
} catch (\Throwable $e) {
|
||||
$this->setError($e->getMessage());
|
||||
|
||||
$result = false;
|
||||
}
|
||||
|
||||
// Decrypt the options (they must be decrypted in memory)
|
||||
$this->decryptOptions();
|
||||
|
||||
if ($result) {
|
||||
// If this record is the default unset the default flag from all other records
|
||||
$this->switchDefaultRecord();
|
||||
|
||||
// Do I need to generate backup codes?
|
||||
if ($mustCreateBackupCodes) {
|
||||
$this->generateBackupCodes();
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to load a row from the database by primary key and bind the fields to the Table instance properties.
|
||||
*
|
||||
* @param mixed $keys An optional primary key value to load the row by, or an array of fields to match.
|
||||
* If not set the instance property value is used.
|
||||
* @param boolean $reset True to reset the default values before loading the new row.
|
||||
*
|
||||
* @return boolean True if successful. False if row not found.
|
||||
*
|
||||
* @since 4.2.0
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function load($keys = null, $reset = true)
|
||||
{
|
||||
$result = parent::load($keys, $reset);
|
||||
|
||||
if ($result) {
|
||||
$this->decryptOptions();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to delete a row from the database table by primary key value.
|
||||
*
|
||||
* @param mixed $pk An optional primary key value to delete. If not set the instance property value is used.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @since 4.2.0
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function delete($pk = null)
|
||||
{
|
||||
$record = $this;
|
||||
|
||||
if ($pk != $this->id) {
|
||||
$record = clone $this;
|
||||
$record->reset();
|
||||
$result = $record->load($pk);
|
||||
|
||||
if (!$result) {
|
||||
// If the record does not exist I will stomp my feet and deny your request
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
}
|
||||
|
||||
$user = $this->getCurrentUser();
|
||||
|
||||
// The user must be a registered user, not a guest
|
||||
if ($user->guest) {
|
||||
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
// Save flags used onAfterDelete
|
||||
$this->deleteFlags[$record->id] = [
|
||||
'default' => $record->default,
|
||||
'numRecords' => $this->getNumRecords($record->user_id),
|
||||
'user_id' => $record->user_id,
|
||||
'method' => $record->method,
|
||||
];
|
||||
|
||||
if (\is_null($pk)) {
|
||||
$pk = [$this->_tbl_key => $this->id];
|
||||
} elseif (!\is_array($pk)) {
|
||||
$pk = [$this->_tbl_key => $pk];
|
||||
}
|
||||
|
||||
$isDeleted = parent::delete($pk);
|
||||
|
||||
if ($isDeleted) {
|
||||
$this->afterDelete($pk);
|
||||
}
|
||||
|
||||
return $isDeleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the possibly encrypted options
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function decryptOptions(): void
|
||||
{
|
||||
// Try with modern decryption
|
||||
$decrypted = @json_decode($this->encryptService->decrypt($this->options ?? ''), true);
|
||||
|
||||
if (\is_string($decrypted)) {
|
||||
$decrypted = @json_decode($decrypted, true);
|
||||
}
|
||||
|
||||
// Fall back to legacy decryption
|
||||
if (!\is_array($decrypted)) {
|
||||
$decrypted = @json_decode($this->encryptService->decrypt($this->options ?? '', true), true);
|
||||
|
||||
if (\is_string($decrypted)) {
|
||||
$decrypted = @json_decode($decrypted, true);
|
||||
}
|
||||
}
|
||||
|
||||
$this->options = $decrypted ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* If this record is set to be the default, unset the default flag from the other records for the same user.
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function switchDefaultRecord(): void
|
||||
{
|
||||
if (!$this->default) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* This record is marked as default, therefore we need to unset the default flag from all other records for this
|
||||
* user.
|
||||
*/
|
||||
$db = $this->getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__user_mfa'))
|
||||
->set($db->quoteName('default') . ' = 0')
|
||||
->where($db->quoteName('user_id') . ' = :user_id')
|
||||
->where($db->quoteName('id') . ' != :id')
|
||||
->bind(':user_id', $this->user_id, ParameterType::INTEGER)
|
||||
->bind(':id', $this->id, ParameterType::INTEGER);
|
||||
$db->setQuery($query)->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate backup code is the flag is set.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function generateBackupCodes(): void
|
||||
{
|
||||
/** @var MVCFactoryInterface $factory */
|
||||
$factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory();
|
||||
|
||||
/** @var BackupcodesModel $backupCodes */
|
||||
$backupCodes = $factory->createModel('Backupcodes', 'Administrator');
|
||||
$user = $this->getUserFactory()->loadUserById($this->user_id);
|
||||
$backupCodes->regenerateBackupCodes($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs after successfully deleting a record
|
||||
*
|
||||
* @param int|array $pk The promary key of the deleted record
|
||||
*
|
||||
* @return void
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function afterDelete($pk): void
|
||||
{
|
||||
if (\is_array($pk)) {
|
||||
$pk = $pk[$this->_tbl_key] ?? array_shift($pk);
|
||||
}
|
||||
|
||||
if (!isset($this->deleteFlags[$pk])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (($this->deleteFlags[$pk]['numRecords'] <= 2) && ($this->deleteFlags[$pk]['method'] != 'backupcodes')) {
|
||||
/**
|
||||
* This was the second to last MFA record in the database (the last one is the `backupcodes`). Therefore, we
|
||||
* need to delete the remaining entry and go away. We don't trigger this if the Method we are deleting was
|
||||
* the `backupcodes` because we might just be regenerating the backup codes.
|
||||
*/
|
||||
$db = $this->getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->delete($db->quoteName('#__user_mfa'))
|
||||
->where($db->quoteName('user_id') . ' = :user_id')
|
||||
->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER);
|
||||
$db->setQuery($query)->execute();
|
||||
|
||||
unset($this->deleteFlags[$pk]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// This was the default record. Promote the next available record to default.
|
||||
if ($this->deleteFlags[$pk]['default']) {
|
||||
$db = $this->getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__user_mfa'))
|
||||
->where($db->quoteName('user_id') . ' = :user_id')
|
||||
->where($db->quoteName('method') . ' != ' . $db->quote('backupcodes'))
|
||||
->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER);
|
||||
$ids = $db->setQuery($query)->loadColumn();
|
||||
|
||||
if (empty($ids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = array_shift($ids);
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__user_mfa'))
|
||||
->set($db->quoteName('default') . ' = 1')
|
||||
->where($db->quoteName('id') . ' = :id')
|
||||
->bind(':id', $id, ParameterType::INTEGER);
|
||||
$db->setQuery($query)->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of MFA records for a give user ID
|
||||
*
|
||||
* @param int $userId The user ID to check
|
||||
*
|
||||
* @return integer
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function getNumRecords(int $userId): int
|
||||
{
|
||||
$db = $this->getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__user_mfa'))
|
||||
->where($db->quoteName('user_id') . ' = :user_id')
|
||||
->bind(':user_id', $userId, ParameterType::INTEGER);
|
||||
$numOldRecords = $db->setQuery($query)->loadResult();
|
||||
|
||||
return (int) $numOldRecords;
|
||||
}
|
||||
}
|
||||
131
administrator/components/com_users/src/Table/NoteTable.php
Normal file
131
administrator/components/com_users/src/Table/NoteTable.php
Normal file
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\Table;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Table\Table;
|
||||
use Joomla\CMS\User\CurrentUserInterface;
|
||||
use Joomla\CMS\User\CurrentUserTrait;
|
||||
use Joomla\CMS\Versioning\VersionableTableInterface;
|
||||
use Joomla\Database\DatabaseDriver;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User notes table class
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
class NoteTable extends Table implements VersionableTableInterface, CurrentUserInterface
|
||||
{
|
||||
use CurrentUserTrait;
|
||||
|
||||
/**
|
||||
* Indicates that columns fully support the NULL value in the database
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.0.0
|
||||
*/
|
||||
protected $_supportNullValue = true;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DatabaseDriver $db Database connector object
|
||||
* @param ?DispatcherInterface $dispatcher Event dispatcher for this table
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
public function __construct(DatabaseDriver $db, DispatcherInterface $dispatcher = null)
|
||||
{
|
||||
$this->typeAlias = 'com_users.note';
|
||||
parent::__construct('#__user_notes', 'id', $db, $dispatcher);
|
||||
|
||||
$this->setColumnAlias('published', 'state');
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloaded store method for the notes table.
|
||||
*
|
||||
* @param boolean $updateNulls Toggle whether null values should be updated.
|
||||
*
|
||||
* @return boolean True on success, false on failure.
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
public function store($updateNulls = true)
|
||||
{
|
||||
$date = Factory::getDate()->toSql();
|
||||
$userId = $this->getCurrentUser()->get('id');
|
||||
|
||||
if (!((int) $this->review_time)) {
|
||||
$this->review_time = null;
|
||||
}
|
||||
|
||||
if ($this->id) {
|
||||
// Existing item
|
||||
$this->modified_time = $date;
|
||||
$this->modified_user_id = $userId;
|
||||
} else {
|
||||
// New record.
|
||||
$this->created_time = $date;
|
||||
$this->created_user_id = $userId;
|
||||
$this->modified_time = $date;
|
||||
$this->modified_user_id = $userId;
|
||||
}
|
||||
|
||||
// Attempt to store the data.
|
||||
return parent::store($updateNulls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to perform sanity checks on the Table instance properties to ensure they are safe to store in the database.
|
||||
*
|
||||
* @return boolean True if the instance is sane and able to be stored in the database.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function check()
|
||||
{
|
||||
try {
|
||||
parent::check();
|
||||
} catch (\Exception $e) {
|
||||
$this->setError($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($this->modified_time)) {
|
||||
$this->modified_time = $this->created_time;
|
||||
}
|
||||
|
||||
if (empty($this->modified_user_id)) {
|
||||
$this->modified_user_id = $this->created_user_id;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type alias for the history table
|
||||
*
|
||||
* @return string The alias as described above
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function getTypeAlias()
|
||||
{
|
||||
return $this->typeAlias;
|
||||
}
|
||||
}
|
||||
219
administrator/components/com_users/src/View/Captive/HtmlView.php
Normal file
219
administrator/components/com_users/src/View/Captive/HtmlView.php
Normal file
@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\Captive;
|
||||
|
||||
use Joomla\CMS\Event\MultiFactor\BeforeDisplayMethods;
|
||||
use Joomla\CMS\Event\MultiFactor\NotifyActionLog;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\CMS\Toolbar\Button\BasicButton;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
|
||||
use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
|
||||
use Joomla\Component\Users\Administrator\Model\CaptiveModel;
|
||||
use Joomla\Component\Users\Administrator\View\SiteTemplateTrait;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* View for Multi-factor Authentication captive page
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
use SiteTemplateTrait;
|
||||
|
||||
/**
|
||||
* The MFA Method records for the current user which correspond to enabled plugins
|
||||
*
|
||||
* @var array
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $records = [];
|
||||
|
||||
/**
|
||||
* The currently selected MFA Method record against which we'll be authenticating
|
||||
*
|
||||
* @var null|\stdClass
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $record = null;
|
||||
|
||||
/**
|
||||
* The Captive MFA page's rendering options
|
||||
*
|
||||
* @var array|null
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $renderOptions = null;
|
||||
|
||||
/**
|
||||
* The title to display at the top of the page
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $title = '';
|
||||
|
||||
/**
|
||||
* Is this an administrator page?
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $isAdmin = false;
|
||||
|
||||
/**
|
||||
* Does the currently selected Method allow authenticating against all of its records?
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $allowEntryBatching = false;
|
||||
|
||||
/**
|
||||
* All enabled MFA Methods (plugins)
|
||||
*
|
||||
* @var array
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $mfaMethods;
|
||||
|
||||
/**
|
||||
* Execute and display a template script.
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void A string if successful, otherwise an Error object.
|
||||
*
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$this->setSiteTemplateStyle();
|
||||
|
||||
$app = Factory::getApplication();
|
||||
$user = $this->getCurrentUser();
|
||||
|
||||
PluginHelper::importPlugin('multifactorauth');
|
||||
$event = new BeforeDisplayMethods($user);
|
||||
$app->getDispatcher()->dispatch($event->getName(), $event);
|
||||
|
||||
/** @var CaptiveModel $model */
|
||||
$model = $this->getModel();
|
||||
|
||||
// Load data from the model
|
||||
$this->isAdmin = $app->isClient('administrator');
|
||||
$this->records = $this->get('records');
|
||||
$this->record = $this->get('record');
|
||||
$this->mfaMethods = MfaHelper::getMfaMethods();
|
||||
|
||||
if (!empty($this->records)) {
|
||||
/** @var BackupcodesModel $codesModel */
|
||||
$codesModel = $this->getModel('Backupcodes');
|
||||
$backupCodesRecord = $codesModel->getBackupCodesRecord();
|
||||
|
||||
if (!\is_null($backupCodesRecord)) {
|
||||
$backupCodesRecord->title = Text::_('COM_USERS_USER_BACKUPCODES');
|
||||
$this->records[] = $backupCodesRecord;
|
||||
}
|
||||
}
|
||||
|
||||
// If we only have one record there's no point asking the user to select a MFA Method
|
||||
if (empty($this->record) && !empty($this->records)) {
|
||||
// Default to the first record
|
||||
$this->record = reset($this->records);
|
||||
|
||||
// If we have multiple records try to make this record the default
|
||||
if (\count($this->records) > 1) {
|
||||
foreach ($this->records as $record) {
|
||||
if ($record->default) {
|
||||
$this->record = $record;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the correct layout based on the availability of a MFA record
|
||||
$this->setLayout('default');
|
||||
|
||||
// If we have no record selected or explicitly asked to run the 'select' task use the correct layout
|
||||
if (\is_null($this->record) || ($model->getState('task') == 'select')) {
|
||||
$this->setLayout('select');
|
||||
}
|
||||
|
||||
switch ($this->getLayout()) {
|
||||
case 'select':
|
||||
$this->allowEntryBatching = 1;
|
||||
|
||||
$event = new NotifyActionLog('onComUsersCaptiveShowSelect', []);
|
||||
Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event);
|
||||
break;
|
||||
|
||||
case 'default':
|
||||
default:
|
||||
$this->renderOptions = $model->loadCaptiveRenderOptions($this->record);
|
||||
$this->allowEntryBatching = $this->renderOptions['allowEntryBatching'] ?? 0;
|
||||
|
||||
$event = new NotifyActionLog(
|
||||
'onComUsersCaptiveShowCaptive',
|
||||
[
|
||||
$this->escape($this->record->title),
|
||||
]
|
||||
);
|
||||
Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event);
|
||||
break;
|
||||
}
|
||||
|
||||
// Which title should I use for the page?
|
||||
$this->title = $this->get('PageTitle');
|
||||
|
||||
// Back-end: always show a title in the 'title' module position, not in the page body
|
||||
if ($this->isAdmin) {
|
||||
ToolbarHelper::title(Text::_('COM_USERS_USER_MULTIFACTOR_AUTH'), 'users user-lock');
|
||||
$this->title = '';
|
||||
}
|
||||
|
||||
if ($this->isAdmin && $this->getLayout() === 'default') {
|
||||
$bar = Toolbar::getInstance();
|
||||
$button = (new BasicButton('user-mfa-submit'))
|
||||
->text($this->renderOptions['submit_text'])
|
||||
->icon($this->renderOptions['submit_icon']);
|
||||
$bar->appendButton($button);
|
||||
|
||||
$button = (new BasicButton('user-mfa-logout'))
|
||||
->text('COM_USERS_MFA_LOGOUT')
|
||||
->buttonClass('btn btn-danger')
|
||||
->icon('icon icon-lock');
|
||||
$bar->appendButton($button);
|
||||
|
||||
if (\count($this->records) > 1) {
|
||||
$arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left';
|
||||
$button = (new BasicButton('user-mfa-choose-another'))
|
||||
->text('COM_USERS_MFA_USE_DIFFERENT_METHOD')
|
||||
->icon('icon-' . $arrow);
|
||||
$bar->appendButton($button);
|
||||
}
|
||||
}
|
||||
|
||||
// Display the view
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\Debuggroup;
|
||||
|
||||
use Joomla\CMS\Access\Exception\NotAllowed;
|
||||
use Joomla\CMS\Helper\ContentHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\GenericDataException;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* View class for a list of User Group ACL permissions.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* List of component actions
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $actions;
|
||||
|
||||
/**
|
||||
* The item data.
|
||||
*
|
||||
* @var object
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $items;
|
||||
|
||||
/**
|
||||
* The pagination object.
|
||||
*
|
||||
* @var \Joomla\CMS\Pagination\Pagination
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $pagination;
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* The id and title for the user group.
|
||||
*
|
||||
* @var \stdClass
|
||||
* @since 4.0.0
|
||||
*/
|
||||
protected $group;
|
||||
|
||||
/**
|
||||
* Form object for search filters
|
||||
*
|
||||
* @var \Joomla\CMS\Form\Form
|
||||
*/
|
||||
public $filterForm;
|
||||
|
||||
/**
|
||||
* The active search filters
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $activeFilters;
|
||||
|
||||
/**
|
||||
* Display the view
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function display($tpl = null)
|
||||
{
|
||||
// Access check.
|
||||
if (!$this->getCurrentUser()->authorise('core.manage', 'com_users')) {
|
||||
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
$this->actions = $this->get('DebugActions');
|
||||
$this->items = $this->get('Items');
|
||||
$this->pagination = $this->get('Pagination');
|
||||
$this->state = $this->get('State');
|
||||
$this->group = $this->get('Group');
|
||||
$this->filterForm = $this->get('FilterForm');
|
||||
$this->activeFilters = $this->get('ActiveFilters');
|
||||
|
||||
// Check for errors.
|
||||
if (\count($errors = $this->get('Errors'))) {
|
||||
throw new GenericDataException(implode("\n", $errors), 500);
|
||||
}
|
||||
|
||||
$this->addToolbar();
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the page title and toolbar.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function addToolbar()
|
||||
{
|
||||
$canDo = ContentHelper::getActions('com_users');
|
||||
$toolbar = Toolbar::getInstance();
|
||||
|
||||
ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_GROUP_TITLE', $this->group->id, $this->escape($this->group->title)), 'users groups');
|
||||
$toolbar->cancel('group.cancel');
|
||||
|
||||
if ($canDo->get('core.admin') || $canDo->get('core.options')) {
|
||||
$toolbar->preferences('com_users');
|
||||
$toolbar->divider();
|
||||
}
|
||||
|
||||
$toolbar->help('Permissions_for_Group');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\Debuguser;
|
||||
|
||||
use Joomla\CMS\Access\Exception\NotAllowed;
|
||||
use Joomla\CMS\Helper\ContentHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\GenericDataException;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
use Joomla\CMS\User\User;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* View class for a list of User ACL permissions.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* List of component actions
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $actions;
|
||||
|
||||
/**
|
||||
* The item data.
|
||||
*
|
||||
* @var object
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $items;
|
||||
|
||||
/**
|
||||
* The pagination object.
|
||||
*
|
||||
* @var \Joomla\CMS\Pagination\Pagination
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $pagination;
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* The user object of the user being debugged.
|
||||
*
|
||||
* @var User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* Form object for search filters
|
||||
*
|
||||
* @var \Joomla\CMS\Form\Form
|
||||
*/
|
||||
public $filterForm;
|
||||
|
||||
/**
|
||||
* The active search filters
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $activeFilters;
|
||||
|
||||
/**
|
||||
* Display the view
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function display($tpl = null)
|
||||
{
|
||||
// Access check.
|
||||
if (!$this->getCurrentUser()->authorise('core.manage', 'com_users')) {
|
||||
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
$this->actions = $this->get('DebugActions');
|
||||
$this->items = $this->get('Items');
|
||||
$this->pagination = $this->get('Pagination');
|
||||
$this->state = $this->get('State');
|
||||
$this->user = $this->get('User');
|
||||
$this->filterForm = $this->get('FilterForm');
|
||||
$this->activeFilters = $this->get('ActiveFilters');
|
||||
|
||||
// Check for errors.
|
||||
if (\count($errors = $this->get('Errors'))) {
|
||||
throw new GenericDataException(implode("\n", $errors), 500);
|
||||
}
|
||||
|
||||
$this->addToolbar();
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the page title and toolbar.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function addToolbar()
|
||||
{
|
||||
$canDo = ContentHelper::getActions('com_users');
|
||||
$toolbar = Toolbar::getInstance();
|
||||
|
||||
ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_USER_TITLE', $this->user->id, $this->escape($this->user->name)), 'users user');
|
||||
$toolbar->cancel('user.cancel');
|
||||
|
||||
if ($canDo->get('core.admin') || $canDo->get('core.options')) {
|
||||
$toolbar->preferences('com_users');
|
||||
$toolbar->divider();
|
||||
}
|
||||
|
||||
$toolbar->help('Permissions_for_User');
|
||||
}
|
||||
}
|
||||
126
administrator/components/com_users/src/View/Group/HtmlView.php
Normal file
126
administrator/components/com_users/src/View/Group/HtmlView.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\Group;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Helper\ContentHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\GenericDataException;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* View to edit a user group.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* The Form object
|
||||
*
|
||||
* @var \Joomla\CMS\Form\Form
|
||||
*/
|
||||
protected $form;
|
||||
|
||||
/**
|
||||
* The item data.
|
||||
*
|
||||
* @var object
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $item;
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* Display the view
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$this->state = $this->get('State');
|
||||
$this->item = $this->get('Item');
|
||||
$this->form = $this->get('Form');
|
||||
|
||||
// Check for errors.
|
||||
if (\count($errors = $this->get('Errors'))) {
|
||||
throw new GenericDataException(implode("\n", $errors), 500);
|
||||
}
|
||||
|
||||
$this->addToolbar();
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the page title and toolbar.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function addToolbar()
|
||||
{
|
||||
Factory::getApplication()->getInput()->set('hidemainmenu', true);
|
||||
|
||||
$isNew = ($this->item->id == 0);
|
||||
$canDo = ContentHelper::getActions('com_users');
|
||||
$toolbar = Toolbar::getInstance();
|
||||
|
||||
ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_GROUP_TITLE' : 'COM_USERS_VIEW_EDIT_GROUP_TITLE'), 'users-cog groups-add');
|
||||
|
||||
if ($canDo->get('core.edit') || $canDo->get('core.create')) {
|
||||
$toolbar->apply('group.apply');
|
||||
}
|
||||
|
||||
$saveGroup = $toolbar->dropdownButton('save-group');
|
||||
$saveGroup->configure(
|
||||
function (Toolbar $childBar) use ($canDo, $isNew) {
|
||||
if ($canDo->get('core.edit') || $canDo->get('core.create')) {
|
||||
$childBar->save('group.save');
|
||||
}
|
||||
|
||||
if ($canDo->get('core.create')) {
|
||||
$childBar->save2new('group.save2new');
|
||||
}
|
||||
|
||||
// If an existing item, can save to a copy.
|
||||
if (!$isNew && $canDo->get('core.create')) {
|
||||
$childBar->save2copy('group.save2copy');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (empty($this->item->id)) {
|
||||
$toolbar->cancel('group.cancel', 'JTOOLBAR_CANCEL');
|
||||
} else {
|
||||
$toolbar->cancel('group.cancel');
|
||||
}
|
||||
|
||||
$toolbar->divider();
|
||||
$toolbar->help('Users:_New_or_Edit_Group');
|
||||
}
|
||||
}
|
||||
127
administrator/components/com_users/src/View/Groups/HtmlView.php
Normal file
127
administrator/components/com_users/src/View/Groups/HtmlView.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\Groups;
|
||||
|
||||
use Joomla\CMS\Helper\ContentHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\GenericDataException;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* View class for a list of user groups.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* The item data.
|
||||
*
|
||||
* @var object
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $items;
|
||||
|
||||
/**
|
||||
* The pagination object.
|
||||
*
|
||||
* @var \Joomla\CMS\Pagination\Pagination
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $pagination;
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* Form object for search filters
|
||||
*
|
||||
* @var \Joomla\CMS\Form\Form
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public $filterForm;
|
||||
|
||||
/**
|
||||
* The active search filters
|
||||
*
|
||||
* @var array
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public $activeFilters;
|
||||
|
||||
/**
|
||||
* Display the view
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$this->items = $this->get('Items');
|
||||
$this->pagination = $this->get('Pagination');
|
||||
$this->state = $this->get('State');
|
||||
$this->filterForm = $this->get('FilterForm');
|
||||
$this->activeFilters = $this->get('ActiveFilters');
|
||||
|
||||
// Check for errors.
|
||||
if (\count($errors = $this->get('Errors'))) {
|
||||
throw new GenericDataException(implode("\n", $errors), 500);
|
||||
}
|
||||
|
||||
$this->addToolbar();
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the page title and toolbar.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function addToolbar()
|
||||
{
|
||||
$canDo = ContentHelper::getActions('com_users');
|
||||
$toolbar = Toolbar::getInstance();
|
||||
|
||||
ToolbarHelper::title(Text::_('COM_USERS_VIEW_GROUPS_TITLE'), 'users-cog groups');
|
||||
|
||||
if ($canDo->get('core.create')) {
|
||||
$toolbar->addNew('group.add');
|
||||
}
|
||||
|
||||
if ($canDo->get('core.delete')) {
|
||||
$toolbar->delete('groups.delete')
|
||||
->message('JGLOBAL_CONFIRM_DELETE');
|
||||
$toolbar->divider();
|
||||
}
|
||||
|
||||
if ($canDo->get('core.admin') || $canDo->get('core.options')) {
|
||||
$toolbar->preferences('com_users');
|
||||
$toolbar->divider();
|
||||
}
|
||||
|
||||
$toolbar->help('Users:_Groups');
|
||||
}
|
||||
}
|
||||
126
administrator/components/com_users/src/View/Level/HtmlView.php
Normal file
126
administrator/components/com_users/src/View/Level/HtmlView.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\Level;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Helper\ContentHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\GenericDataException;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* View to edit a user view level.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* The Form object
|
||||
*
|
||||
* @var \Joomla\CMS\Form\Form
|
||||
*/
|
||||
protected $form;
|
||||
|
||||
/**
|
||||
* The item data.
|
||||
*
|
||||
* @var object
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $item;
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* Display the view
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$this->form = $this->get('Form');
|
||||
$this->item = $this->get('Item');
|
||||
$this->state = $this->get('State');
|
||||
|
||||
// Check for errors.
|
||||
if (\count($errors = $this->get('Errors'))) {
|
||||
throw new GenericDataException(implode("\n", $errors), 500);
|
||||
}
|
||||
|
||||
$this->addToolbar();
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the page title and toolbar.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function addToolbar()
|
||||
{
|
||||
Factory::getApplication()->getInput()->set('hidemainmenu', true);
|
||||
|
||||
$isNew = ($this->item->id == 0);
|
||||
$canDo = ContentHelper::getActions('com_users');
|
||||
$toolbar = Toolbar::getInstance();
|
||||
|
||||
ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_LEVEL_TITLE' : 'COM_USERS_VIEW_EDIT_LEVEL_TITLE'), 'user-lock levels-add');
|
||||
|
||||
if ($canDo->get('core.edit') || $canDo->get('core.create')) {
|
||||
$toolbar->apply('level.apply');
|
||||
}
|
||||
|
||||
$saveGroup = $toolbar->dropdownButton('save-group');
|
||||
$saveGroup->configure(
|
||||
function (Toolbar $childBar) use ($canDo, $isNew) {
|
||||
if ($canDo->get('core.edit') || $canDo->get('core.create')) {
|
||||
$childBar->save('level.save');
|
||||
}
|
||||
|
||||
if ($canDo->get('core.create')) {
|
||||
$childBar->save2new('level.save2new');
|
||||
}
|
||||
|
||||
// If an existing item, can save to a copy.
|
||||
if (!$isNew && $canDo->get('core.create')) {
|
||||
$childBar->save2copy('level.save2copy');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (empty($this->item->id)) {
|
||||
$toolbar->cancel('level.cancel', 'JTOOLBAR_CANCEL');
|
||||
} else {
|
||||
$toolbar->cancel('level.cancel');
|
||||
}
|
||||
|
||||
$toolbar->divider();
|
||||
$toolbar->help('Users:_Edit_Viewing_Access_Level');
|
||||
}
|
||||
}
|
||||
127
administrator/components/com_users/src/View/Levels/HtmlView.php
Normal file
127
administrator/components/com_users/src/View/Levels/HtmlView.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\Levels;
|
||||
|
||||
use Joomla\CMS\Helper\ContentHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\GenericDataException;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* View class for a list of view levels.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* The item data.
|
||||
*
|
||||
* @var object
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $items;
|
||||
|
||||
/**
|
||||
* The pagination object.
|
||||
*
|
||||
* @var \Joomla\CMS\Pagination\Pagination
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $pagination;
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* Form object for search filters
|
||||
*
|
||||
* @var \Joomla\CMS\Form\Form
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public $filterForm;
|
||||
|
||||
/**
|
||||
* The active search filters
|
||||
*
|
||||
* @var array
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public $activeFilters;
|
||||
|
||||
/**
|
||||
* Display the view
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$this->items = $this->get('Items');
|
||||
$this->pagination = $this->get('Pagination');
|
||||
$this->state = $this->get('State');
|
||||
$this->filterForm = $this->get('FilterForm');
|
||||
$this->activeFilters = $this->get('ActiveFilters');
|
||||
|
||||
// Check for errors.
|
||||
if (\count($errors = $this->get('Errors'))) {
|
||||
throw new GenericDataException(implode("\n", $errors), 500);
|
||||
}
|
||||
|
||||
$this->addToolbar();
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the page title and toolbar.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function addToolbar()
|
||||
{
|
||||
$canDo = ContentHelper::getActions('com_users');
|
||||
$toolbar = Toolbar::getInstance();
|
||||
|
||||
ToolbarHelper::title(Text::_('COM_USERS_VIEW_LEVELS_TITLE'), 'user-lock levels');
|
||||
|
||||
if ($canDo->get('core.create')) {
|
||||
$toolbar->addNew('level.add');
|
||||
}
|
||||
|
||||
if ($canDo->get('core.delete')) {
|
||||
$toolbar->delete('level.delete')
|
||||
->message('JGLOBAL_CONFIRM_DELETE');
|
||||
$toolbar->divider();
|
||||
}
|
||||
|
||||
if ($canDo->get('core.admin') || $canDo->get('core.options')) {
|
||||
$toolbar->preferences('com_users');
|
||||
$toolbar->divider();
|
||||
}
|
||||
|
||||
$toolbar->help('Users:_Viewing_Access_Levels');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\Mail;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Users mail view.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* The Form object
|
||||
*
|
||||
* @var \Joomla\CMS\Form\Form
|
||||
*/
|
||||
protected $form;
|
||||
|
||||
/**
|
||||
* Display the view
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function display($tpl = null)
|
||||
{
|
||||
// Redirect to admin index if mass mailer disabled in conf
|
||||
if (Factory::getApplication()->get('massmailoff', 0) == 1) {
|
||||
Factory::getApplication()->redirect(Route::_('index.php', false));
|
||||
}
|
||||
|
||||
// Get data from the model
|
||||
$this->form = $this->get('Form');
|
||||
|
||||
$this->addToolbar();
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the page title and toolbar.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function addToolbar()
|
||||
{
|
||||
Factory::getApplication()->getInput()->set('hidemainmenu', true);
|
||||
|
||||
ToolbarHelper::title(Text::_('COM_USERS_MASS_MAIL'), 'users massmail');
|
||||
$toolbar = Toolbar::getInstance();
|
||||
$toolbar->standardButton('COM_USERS_TOOLBAR_MAIL_SEND_MAIL', 'COM_USERS_TOOLBAR_MAIL_SEND_MAIL', 'mail.send')
|
||||
->icon('icon-envelope')
|
||||
->formValidation(true);
|
||||
|
||||
$toolbar->cancel('mail.cancel', 'JTOOLBAR_CANCEL');
|
||||
$toolbar->divider();
|
||||
$toolbar->preferences('com_users');
|
||||
$toolbar->divider();
|
||||
$toolbar->help('Mass_Mail_Users');
|
||||
}
|
||||
}
|
||||
220
administrator/components/com_users/src/View/Method/HtmlView.php
Normal file
220
administrator/components/com_users/src/View/Method/HtmlView.php
Normal file
@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\Method;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Toolbar\Button\BasicButton;
|
||||
use Joomla\CMS\Toolbar\Button\LinkButton;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
use Joomla\CMS\User\User;
|
||||
use Joomla\Component\Users\Administrator\Model\MethodModel;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* View for Multi-factor Authentication method add/edit page
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* Is this an administrator page?
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $isAdmin = false;
|
||||
|
||||
/**
|
||||
* The editor page render options
|
||||
*
|
||||
* @var array
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $renderOptions = [];
|
||||
|
||||
/**
|
||||
* The MFA Method record being edited
|
||||
*
|
||||
* @var object
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $record = null;
|
||||
|
||||
/**
|
||||
* The title text for this page
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $title = '';
|
||||
|
||||
/**
|
||||
* The return URL to use for all links and forms
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $returnURL = null;
|
||||
|
||||
/**
|
||||
* The user object used to display this page
|
||||
*
|
||||
* @var User
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $user = null;
|
||||
|
||||
/**
|
||||
* The backup codes for the current user. Only applies when the backup codes record is being "edited"
|
||||
*
|
||||
* @var array
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $backupCodes = [];
|
||||
|
||||
/**
|
||||
* Am I editing an existing Method? If it's false then I'm adding a new Method.
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $isEditExisting = false;
|
||||
|
||||
/**
|
||||
* Execute and display a template script.
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception
|
||||
* @see \JViewLegacy::loadTemplate()
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
|
||||
if (empty($this->user)) {
|
||||
$this->user = $this->getCurrentUser();
|
||||
}
|
||||
|
||||
/** @var MethodModel $model */
|
||||
$model = $this->getModel();
|
||||
$this->setLayout('edit');
|
||||
$this->renderOptions = $model->getRenderOptions($this->user);
|
||||
$this->record = $model->getRecord($this->user);
|
||||
$this->title = $model->getPageTitle();
|
||||
$this->isAdmin = $app->isClient('administrator');
|
||||
$toolbar = Toolbar::getInstance();
|
||||
|
||||
// Backup codes are a special case, rendered with a special layout
|
||||
if ($this->record->method == 'backupcodes') {
|
||||
$this->setLayout('backupcodes');
|
||||
|
||||
$backupCodes = $this->record->options;
|
||||
|
||||
if (!\is_array($backupCodes)) {
|
||||
$backupCodes = [];
|
||||
}
|
||||
|
||||
$backupCodes = array_filter(
|
||||
$backupCodes,
|
||||
function ($x) {
|
||||
return !empty($x);
|
||||
}
|
||||
);
|
||||
|
||||
if (\count($backupCodes) % 2 != 0) {
|
||||
$backupCodes[] = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* The call to array_merge resets the array indices. This is necessary since array_filter kept the indices,
|
||||
* meaning our elements are completely out of order.
|
||||
*/
|
||||
$this->backupCodes = array_merge($backupCodes);
|
||||
}
|
||||
|
||||
// Set up the isEditExisting property.
|
||||
$this->isEditExisting = !empty($this->record->id);
|
||||
|
||||
// Back-end: always show a title in the 'title' module position, not in the page body
|
||||
if ($this->isAdmin) {
|
||||
ToolbarHelper::title($this->title, 'users user-lock');
|
||||
|
||||
$helpUrl = $this->renderOptions['help_url'];
|
||||
|
||||
if (!empty($helpUrl)) {
|
||||
$toolbar->help('', false, $helpUrl);
|
||||
}
|
||||
|
||||
$this->title = '';
|
||||
}
|
||||
|
||||
$returnUrl = empty($this->returnURL) ? '' : base64_decode($this->returnURL);
|
||||
$returnUrl = ($returnUrl && Uri::isInternal($returnUrl))
|
||||
? $returnUrl
|
||||
: Route::_('index.php?option=com_users&task=methods.display&user_id=' . $this->user->id);
|
||||
|
||||
if ($this->isAdmin && $this->getLayout() === 'edit') {
|
||||
$button = (new BasicButton('user-mfa-edit-save'))
|
||||
->text($this->renderOptions['submit_text'])
|
||||
->icon($this->renderOptions['submit_icon'])
|
||||
->onclick('document.getElementById(\'user-mfa-edit-save\').click()');
|
||||
|
||||
if ($this->renderOptions['show_submit'] || $this->isEditExisting) {
|
||||
$toolbar->appendButton($button);
|
||||
}
|
||||
|
||||
$button = (new LinkButton('user-mfa-edit-cancel'))
|
||||
->url($returnUrl)
|
||||
->text('JCANCEL')
|
||||
->buttonClass('btn btn-danger')
|
||||
->icon('icon-cancel-2');
|
||||
$toolbar->appendButton($button);
|
||||
} elseif ($this->isAdmin && $this->getLayout() === 'backupcodes') {
|
||||
$arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left';
|
||||
$button = (new LinkButton('user-mfa-edit-cancel'))
|
||||
->url($returnUrl)
|
||||
->text('JTOOLBAR_BACK')
|
||||
->icon('icon-' . $arrow);
|
||||
$toolbar->appendButton($button);
|
||||
|
||||
$button = (new LinkButton('user-mfa-edit-cancel'))
|
||||
->url(
|
||||
Route::_(
|
||||
sprintf(
|
||||
"index.php?option=com_users&task=method.regenerateBackupCodes&user_id=%s&%s=1&returnurl=%s",
|
||||
$this->user->id,
|
||||
Factory::getApplication()->getFormToken(),
|
||||
base64_encode($returnUrl)
|
||||
)
|
||||
)
|
||||
)
|
||||
->text('COM_USERS_MFA_BACKUPCODES_RESET')
|
||||
->buttonClass('btn btn-danger')
|
||||
->icon('icon-refresh');
|
||||
$toolbar->appendButton($button);
|
||||
}
|
||||
|
||||
// Display the view
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
196
administrator/components/com_users/src/View/Methods/HtmlView.php
Normal file
196
administrator/components/com_users/src/View/Methods/HtmlView.php
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\Methods;
|
||||
|
||||
use Joomla\CMS\Event\MultiFactor\NotifyActionLog;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
use Joomla\CMS\User\User;
|
||||
use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor;
|
||||
use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
|
||||
use Joomla\Component\Users\Administrator\Model\MethodsModel;
|
||||
use Joomla\Component\Users\Administrator\View\SiteTemplateTrait;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* View for Multi-factor Authentication methods list page
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
use SiteTemplateTrait;
|
||||
|
||||
/**
|
||||
* Is this an administrator page?
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $isAdmin = false;
|
||||
|
||||
/**
|
||||
* The MFA Methods available for this user
|
||||
*
|
||||
* @var array
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $methods = [];
|
||||
|
||||
/**
|
||||
* The return URL to use for all links and forms
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $returnURL = null;
|
||||
|
||||
/**
|
||||
* Are there any active MFA Methods at all?
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $mfaActive = false;
|
||||
|
||||
/**
|
||||
* Which Method has the default record?
|
||||
*
|
||||
* @var string
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $defaultMethod = '';
|
||||
|
||||
/**
|
||||
* The user object used to display this page
|
||||
*
|
||||
* @var User
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $user = null;
|
||||
|
||||
/**
|
||||
* Is this page part of the mandatory Multi-factor Authentication setup?
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public $isMandatoryMFASetup = false;
|
||||
|
||||
/**
|
||||
* Execute and display a template script.
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception
|
||||
* @see \JViewLegacy::loadTemplate()
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
$this->setSiteTemplateStyle();
|
||||
|
||||
$app = Factory::getApplication();
|
||||
|
||||
if (empty($this->user)) {
|
||||
$this->user = $this->getCurrentUser();
|
||||
}
|
||||
|
||||
/** @var MethodsModel $model */
|
||||
$model = $this->getModel();
|
||||
|
||||
if ($this->getLayout() !== 'firsttime') {
|
||||
$this->setLayout('default');
|
||||
}
|
||||
|
||||
$this->methods = $model->getMethods($this->user);
|
||||
$this->isAdmin = $app->isClient('administrator');
|
||||
$activeRecords = 0;
|
||||
|
||||
foreach ($this->methods as $methodName => $method) {
|
||||
$methodActiveRecords = \count($method['active']);
|
||||
|
||||
if (!$methodActiveRecords) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$activeRecords += $methodActiveRecords;
|
||||
$this->mfaActive = true;
|
||||
|
||||
foreach ($method['active'] as $record) {
|
||||
if ($record->default) {
|
||||
$this->defaultMethod = $methodName;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no backup codes yet we should create new ones
|
||||
/** @var BackupcodesModel $model */
|
||||
$model = $this->getModel('backupcodes');
|
||||
$backupCodes = $model->getBackupCodes($this->user);
|
||||
|
||||
if ($activeRecords && empty($backupCodes)) {
|
||||
$model->regenerateBackupCodes($this->user);
|
||||
}
|
||||
|
||||
$backupCodesRecord = $model->getBackupCodesRecord($this->user);
|
||||
|
||||
if (!\is_null($backupCodesRecord)) {
|
||||
$this->methods = array_merge(
|
||||
[
|
||||
'backupcodes' => new MethodDescriptor(
|
||||
[
|
||||
'name' => 'backupcodes',
|
||||
'display' => Text::_('COM_USERS_USER_BACKUPCODES'),
|
||||
'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'),
|
||||
'image' => 'media/com_users/images/emergency.svg',
|
||||
'canDisable' => false,
|
||||
'active' => [$backupCodesRecord],
|
||||
]
|
||||
),
|
||||
],
|
||||
$this->methods
|
||||
);
|
||||
}
|
||||
|
||||
$this->isMandatoryMFASetup = $activeRecords === 0 && $app->getSession()->get('com_users.mandatory_mfa_setup', 0) === 1;
|
||||
|
||||
// Back-end: always show a title in the 'title' module position, not in the page body
|
||||
if ($this->isAdmin) {
|
||||
ToolbarHelper::title(Text::_('COM_USERS_MFA_LIST_PAGE_HEAD'), 'users user-lock');
|
||||
|
||||
if ($this->getCurrentUser()->authorise('core.manage', 'com_users')) {
|
||||
$toolbar = Toolbar::getInstance();
|
||||
$arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left';
|
||||
$toolbar->link('JTOOLBAR_BACK', 'index.php?option=com_users')
|
||||
->icon('icon-' . $arrow);
|
||||
}
|
||||
}
|
||||
|
||||
// Display the view
|
||||
parent::display($tpl);
|
||||
|
||||
$event = new NotifyActionLog('onComUsersViewMethodsAfterDisplay', [$this]);
|
||||
Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event);
|
||||
|
||||
Text::script('JGLOBAL_CONFIRM_DELETE');
|
||||
}
|
||||
}
|
||||
145
administrator/components/com_users/src/View/Note/HtmlView.php
Normal file
145
administrator/components/com_users/src/View/Note/HtmlView.php
Normal file
@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\Note;
|
||||
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Helper\ContentHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\GenericDataException;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User note edit view
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* The edit form.
|
||||
*
|
||||
* @var \Joomla\CMS\Form\Form
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
protected $form;
|
||||
|
||||
/**
|
||||
* The item data.
|
||||
*
|
||||
* @var object
|
||||
* @since 2.5
|
||||
*/
|
||||
protected $item;
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 2.5
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* Override the display method for the view.
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 2.5
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function display($tpl = null)
|
||||
{
|
||||
// Initialise view variables.
|
||||
$this->state = $this->get('State');
|
||||
$this->item = $this->get('Item');
|
||||
$this->form = $this->get('Form');
|
||||
|
||||
// Check for errors.
|
||||
if (\count($errors = $this->get('Errors'))) {
|
||||
throw new GenericDataException(implode("\n", $errors), 500);
|
||||
}
|
||||
|
||||
parent::display($tpl);
|
||||
$this->addToolbar();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the toolbar.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 2.5
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function addToolbar()
|
||||
{
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$input->set('hidemainmenu', 1);
|
||||
|
||||
$user = $this->getCurrentUser();
|
||||
$isNew = ($this->item->id == 0);
|
||||
$checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id'));
|
||||
$toolbar = Toolbar::getInstance();
|
||||
|
||||
// Since we don't track these assets at the item level, use the category id.
|
||||
$canDo = ContentHelper::getActions('com_users', 'category', $this->item->catid);
|
||||
|
||||
ToolbarHelper::title(Text::_('COM_USERS_NOTES'), 'users user');
|
||||
|
||||
// If not checked out, can save the item.
|
||||
if (!$checkedOut && ($canDo->get('core.edit') || \count($user->getAuthorisedCategories('com_users', 'core.create')))) {
|
||||
$toolbar->apply('note.apply');
|
||||
}
|
||||
|
||||
$saveGroup = $toolbar->dropdownButton('save-group');
|
||||
|
||||
$saveGroup->configure(
|
||||
function (Toolbar $childBar) use ($checkedOut, $canDo, $user, $isNew) {
|
||||
// If not checked out, can save the item.
|
||||
if (!$checkedOut && ($canDo->get('core.edit') || \count($user->getAuthorisedCategories('com_users', 'core.create')))) {
|
||||
$childBar->save('note.save');
|
||||
}
|
||||
|
||||
if (!$checkedOut && \count($user->getAuthorisedCategories('com_users', 'core.create'))) {
|
||||
$childBar->save2new('note.save2new');
|
||||
}
|
||||
|
||||
// If an existing item, can save to a copy.
|
||||
if (!$isNew && (\count($user->getAuthorisedCategories('com_users', 'core.create')) > 0)) {
|
||||
$childBar->save2copy('note.save2copy');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (empty($this->item->id)) {
|
||||
$toolbar->cancel('note.cancel', 'JTOOLBAR_CANCEL');
|
||||
} else {
|
||||
$toolbar->cancel('note.cancel');
|
||||
|
||||
if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) {
|
||||
$toolbar->versions('com_users.note', $this->item->id);
|
||||
}
|
||||
}
|
||||
|
||||
$toolbar->divider();
|
||||
$toolbar->help('User_Notes:_New_or_Edit');
|
||||
}
|
||||
}
|
||||
180
administrator/components/com_users/src/View/Notes/HtmlView.php
Normal file
180
administrator/components/com_users/src/View/Notes/HtmlView.php
Normal file
@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\Notes;
|
||||
|
||||
use Joomla\CMS\Helper\ContentHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\GenericDataException;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\Button\DropdownButton;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
use Joomla\CMS\User\User;
|
||||
use Joomla\Registry\Registry;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User notes list view
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* A list of user note objects.
|
||||
*
|
||||
* @var array
|
||||
* @since 2.5
|
||||
*/
|
||||
protected $items;
|
||||
|
||||
/**
|
||||
* The pagination object.
|
||||
*
|
||||
* @var \Joomla\CMS\Pagination\Pagination
|
||||
* @since 2.5
|
||||
*/
|
||||
protected $pagination;
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 2.5
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
*
|
||||
* @var User
|
||||
* @since 2.5
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* Form object for search filters
|
||||
*
|
||||
* @var \Joomla\CMS\Form\Form
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public $filterForm;
|
||||
|
||||
/**
|
||||
* The active search filters
|
||||
*
|
||||
* @var array
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public $activeFilters;
|
||||
|
||||
/**
|
||||
* Is this view an Empty State
|
||||
*
|
||||
* @var boolean
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private $isEmptyState = false;
|
||||
|
||||
/**
|
||||
* Override the display method for the view.
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
public function display($tpl = null)
|
||||
{
|
||||
// Initialise view variables.
|
||||
$this->items = $this->get('Items');
|
||||
$this->pagination = $this->get('Pagination');
|
||||
$this->state = $this->get('State');
|
||||
$this->user = $this->get('User');
|
||||
$this->filterForm = $this->get('FilterForm');
|
||||
$this->activeFilters = $this->get('ActiveFilters');
|
||||
|
||||
if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) {
|
||||
$this->setLayout('emptystate');
|
||||
}
|
||||
|
||||
// Check for errors.
|
||||
if (\count($errors = $this->get('Errors'))) {
|
||||
throw new GenericDataException(implode("\n", $errors), 500);
|
||||
}
|
||||
|
||||
// Turn parameters into registry objects
|
||||
foreach ($this->items as $item) {
|
||||
$item->cparams = new Registry($item->category_params);
|
||||
}
|
||||
|
||||
$this->addToolbar();
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the toolbar.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
protected function addToolbar()
|
||||
{
|
||||
$canDo = ContentHelper::getActions('com_users', 'category', $this->state->get('filter.category_id'));
|
||||
$toolbar = Toolbar::getInstance();
|
||||
|
||||
ToolbarHelper::title(Text::_('COM_USERS_VIEW_NOTES_TITLE'), 'users user');
|
||||
|
||||
if ($canDo->get('core.create')) {
|
||||
$toolbar->addNew('note.add');
|
||||
}
|
||||
|
||||
if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) {
|
||||
/** @var DropdownButton $dropdown */
|
||||
$dropdown = $toolbar->dropdownButton('status-group', 'JTOOLBAR_CHANGE_STATUS')
|
||||
->toggleSplit(false)
|
||||
->icon('icon-ellipsis-h')
|
||||
->buttonClass('btn btn-action')
|
||||
->listCheck(true);
|
||||
|
||||
$childBar = $dropdown->getChildToolbar();
|
||||
|
||||
if ($canDo->get('core.edit.state')) {
|
||||
$childBar->publish('notes.publish')->listCheck(true);
|
||||
$childBar->unpublish('notes.unpublish')->listCheck(true);
|
||||
$childBar->archive('notes.archive')->listCheck(true);
|
||||
$childBar->checkin('notes.checkin')->listCheck(true);
|
||||
}
|
||||
|
||||
if ($this->state->get('filter.published') != -2 && $canDo->get('core.edit.state')) {
|
||||
$childBar->trash('notes.trash');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) {
|
||||
$toolbar->delete('notes.delete', 'JTOOLBAR_EMPTY_TRASH')
|
||||
->message('JGLOBAL_CONFIRM_DELETE')
|
||||
->listCheck(true);
|
||||
}
|
||||
|
||||
if ($canDo->get('core.admin') || $canDo->get('core.options')) {
|
||||
$toolbar->preferences('com_users');
|
||||
}
|
||||
|
||||
$toolbar->help('User_Notes');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View;
|
||||
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Factory;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Dynamically modify the frontend template when showing a MFA captive page.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
trait SiteTemplateTrait
|
||||
{
|
||||
/**
|
||||
* Set a specific site template style in the frontend application
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @since 4.2.0
|
||||
*/
|
||||
private function setSiteTemplateStyle(): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$templateStyle = (int) ComponentHelper::getParams('com_users')->get('captive_template', '');
|
||||
|
||||
if (empty($templateStyle) || !$app->isClient('site')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$itemId = $app->getInput()->get('Itemid');
|
||||
|
||||
if (!empty($itemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$app->getInput()->set('templateStyle', $templateStyle);
|
||||
|
||||
try {
|
||||
$refApp = new \ReflectionObject($app);
|
||||
$refTemplate = $refApp->getProperty('template');
|
||||
$refTemplate->setAccessible(true);
|
||||
$refTemplate->setValue($app, null);
|
||||
} catch (\ReflectionException $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$template = $app->getTemplate(true);
|
||||
|
||||
$app->set('theme', $template->template);
|
||||
$app->set('themeParams', $template->params);
|
||||
}
|
||||
}
|
||||
188
administrator/components/com_users/src/View/User/HtmlView.php
Normal file
188
administrator/components/com_users/src/View/User/HtmlView.php
Normal file
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2007 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\User;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Helper\ContentHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\GenericDataException;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
use Joomla\CMS\User\UserFactoryAwareInterface;
|
||||
use Joomla\CMS\User\UserFactoryAwareTrait;
|
||||
use Joomla\Component\Users\Administrator\Helper\Mfa;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* User view class.
|
||||
*
|
||||
* @since 1.5
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView implements UserFactoryAwareInterface
|
||||
{
|
||||
use UserFactoryAwareTrait;
|
||||
|
||||
/**
|
||||
* The Form object
|
||||
*
|
||||
* @var \Joomla\CMS\Form\Form
|
||||
*/
|
||||
protected $form;
|
||||
|
||||
/**
|
||||
* The active item
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $item;
|
||||
|
||||
/**
|
||||
* Gets the available groups
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $grouplist;
|
||||
|
||||
/**
|
||||
* The groups this user is assigned to
|
||||
*
|
||||
* @var array
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $groups;
|
||||
|
||||
/**
|
||||
* The model state
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* The Multi-factor Authentication configuration interface for the user.
|
||||
*
|
||||
* @var string|null
|
||||
* @since 4.2.0
|
||||
*/
|
||||
protected $mfaConfigurationUI;
|
||||
|
||||
/**
|
||||
* Display the view
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.5
|
||||
*/
|
||||
public function display($tpl = null)
|
||||
{
|
||||
// If no item found, dont show the edit screen, redirect with message
|
||||
if (false === $this->item = $this->get('Item')) {
|
||||
$app = Factory::getApplication();
|
||||
$app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_NOT_EXIST'), 'error');
|
||||
$app->redirect('index.php?option=com_users&view=users');
|
||||
}
|
||||
|
||||
$this->form = $this->get('Form');
|
||||
$this->state = $this->get('State');
|
||||
|
||||
// Check for errors.
|
||||
if (\count($errors = $this->get('Errors'))) {
|
||||
throw new GenericDataException(implode("\n", $errors), 500);
|
||||
}
|
||||
|
||||
// Prevent user from modifying own group(s)
|
||||
$user = $this->getCurrentUser();
|
||||
|
||||
if ((int) $user->id != (int) $this->item->id || $user->authorise('core.admin')) {
|
||||
$this->grouplist = $this->get('Groups');
|
||||
$this->groups = $this->get('AssignedGroups');
|
||||
}
|
||||
|
||||
$this->form->setValue('password', null);
|
||||
$this->form->setValue('password2', null);
|
||||
|
||||
$userBeingEdited = $this->getUserFactory()->loadUserById($this->item->id);
|
||||
|
||||
if ($this->item->id > 0 && (int) $userBeingEdited->id == (int) $this->item->id) {
|
||||
try {
|
||||
$this->mfaConfigurationUI = Mfa::canShowConfigurationInterface($userBeingEdited)
|
||||
? Mfa::getConfigurationInterface($userBeingEdited)
|
||||
: '';
|
||||
} catch (\Exception $e) {
|
||||
// In case something goes really wrong with the plugins; prevents hard breaks.
|
||||
$this->mfaConfigurationUI = null;
|
||||
}
|
||||
}
|
||||
|
||||
parent::display($tpl);
|
||||
|
||||
$this->addToolbar();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the page title and toolbar.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function addToolbar()
|
||||
{
|
||||
Factory::getApplication()->getInput()->set('hidemainmenu', true);
|
||||
|
||||
$user = $this->getCurrentUser();
|
||||
$canDo = ContentHelper::getActions('com_users');
|
||||
$isNew = ($this->item->id == 0);
|
||||
$isProfile = $this->item->id == $user->id;
|
||||
$toolbar = Toolbar::getInstance();
|
||||
|
||||
ToolbarHelper::title(
|
||||
Text::_(
|
||||
$isNew ? 'COM_USERS_VIEW_NEW_USER_TITLE' : ($isProfile ? 'COM_USERS_VIEW_EDIT_PROFILE_TITLE' : 'COM_USERS_VIEW_EDIT_USER_TITLE')
|
||||
),
|
||||
'user ' . ($isNew ? 'user-add' : ($isProfile ? 'user-profile' : 'user-edit'))
|
||||
);
|
||||
|
||||
if ($canDo->get('core.edit') || $canDo->get('core.create') || $isProfile) {
|
||||
$toolbar->apply('user.apply');
|
||||
}
|
||||
|
||||
$saveGroup = $toolbar->dropdownButton('save-group');
|
||||
|
||||
$saveGroup->configure(
|
||||
function (Toolbar $childBar) use ($canDo, $isProfile) {
|
||||
if ($canDo->get('core.edit') || $canDo->get('core.create') || $isProfile) {
|
||||
$childBar->save('user.save');
|
||||
}
|
||||
|
||||
if ($canDo->get('core.create') && $canDo->get('core.manage')) {
|
||||
$childBar->save2new('user.save2new');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (empty($this->item->id)) {
|
||||
$toolbar->cancel('user.cancel', 'JTOOLBAR_CANCEL');
|
||||
} else {
|
||||
$toolbar->cancel('user.cancel');
|
||||
}
|
||||
|
||||
$toolbar->divider();
|
||||
$toolbar->help('Users:_Edit_Profile');
|
||||
}
|
||||
}
|
||||
181
administrator/components/com_users/src/View/Users/HtmlView.php
Normal file
181
administrator/components/com_users/src/View/Users/HtmlView.php
Normal file
@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Administrator
|
||||
* @subpackage com_users
|
||||
*
|
||||
* @copyright (C) 2007 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\Users\Administrator\View\Users;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Helper\ContentHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\GenericDataException;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\Button\DropdownButton;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
use Joomla\Database\DatabaseDriver;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* View class for a list of users.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* The item data.
|
||||
*
|
||||
* @var object
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $items;
|
||||
|
||||
/**
|
||||
* The pagination object.
|
||||
*
|
||||
* @var \Joomla\CMS\Pagination\Pagination
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $pagination;
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 1.6
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* A Form instance with filter fields.
|
||||
*
|
||||
* @var \Joomla\CMS\Form\Form
|
||||
*
|
||||
* @since 3.6.3
|
||||
*/
|
||||
public $filterForm;
|
||||
|
||||
/**
|
||||
* An array with active filters.
|
||||
*
|
||||
* @var array
|
||||
* @since 3.6.3
|
||||
*/
|
||||
public $activeFilters;
|
||||
|
||||
/**
|
||||
* An ACL object to verify user rights.
|
||||
*
|
||||
* @var \Joomla\Registry\Registry
|
||||
* @since 3.6.3
|
||||
*/
|
||||
protected $canDo;
|
||||
|
||||
/**
|
||||
* An instance of DatabaseDriver.
|
||||
*
|
||||
* @var DatabaseDriver
|
||||
* @since 3.6.3
|
||||
*
|
||||
* @deprecated 4.3 will be removed in 6.0
|
||||
* Will be removed without replacement use database from the container instead
|
||||
* Example: Factory::getContainer()->get(DatabaseInterface::class);
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* Display the view
|
||||
*
|
||||
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$this->items = $this->get('Items');
|
||||
$this->pagination = $this->get('Pagination');
|
||||
$this->state = $this->get('State');
|
||||
$this->filterForm = $this->get('FilterForm');
|
||||
$this->activeFilters = $this->get('ActiveFilters');
|
||||
$this->canDo = ContentHelper::getActions('com_users');
|
||||
$this->db = Factory::getDbo();
|
||||
|
||||
// Check for errors.
|
||||
if (\count($errors = $this->get('Errors'))) {
|
||||
throw new GenericDataException(implode("\n", $errors), 500);
|
||||
}
|
||||
|
||||
$this->addToolbar();
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the page title and toolbar.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
protected function addToolbar()
|
||||
{
|
||||
$canDo = $this->canDo;
|
||||
$user = $this->getCurrentUser();
|
||||
|
||||
// Get the toolbar object instance
|
||||
$toolbar = Toolbar::getInstance('toolbar');
|
||||
|
||||
ToolbarHelper::title(Text::_('COM_USERS_VIEW_USERS_TITLE'), 'users user');
|
||||
|
||||
if ($canDo->get('core.create')) {
|
||||
$toolbar->addNew('user.add');
|
||||
}
|
||||
|
||||
if ($canDo->get('core.edit.state') || $canDo->get('core.admin')) {
|
||||
/** @var DropdownButton $dropdown */
|
||||
$dropdown = $toolbar->dropdownButton('status-group', 'JTOOLBAR_CHANGE_STATUS')
|
||||
->toggleSplit(false)
|
||||
->icon('icon-ellipsis-h')
|
||||
->buttonClass('btn btn-action')
|
||||
->listCheck(true);
|
||||
|
||||
$childBar = $dropdown->getChildToolbar();
|
||||
|
||||
$childBar->publish('users.activate', 'COM_USERS_TOOLBAR_ACTIVATE');
|
||||
$childBar->unpublish('users.block', 'COM_USERS_TOOLBAR_BLOCK');
|
||||
$childBar->standardButton('unblock', 'COM_USERS_TOOLBAR_UNBLOCK', 'users.unblock')
|
||||
->listCheck(true);
|
||||
|
||||
// Add a batch button
|
||||
if (
|
||||
$user->authorise('core.create', 'com_users')
|
||||
&& $user->authorise('core.edit', 'com_users')
|
||||
&& $user->authorise('core.edit.state', 'com_users')
|
||||
) {
|
||||
$childBar->popupButton('batch', 'JTOOLBAR_BATCH')
|
||||
->selector('collapseModal')
|
||||
->listCheck(true);
|
||||
}
|
||||
|
||||
if ($canDo->get('core.delete')) {
|
||||
$childBar->delete('users.delete', 'JTOOLBAR_DELETE')
|
||||
->message('JGLOBAL_CONFIRM_DELETE')
|
||||
->listCheck(true);
|
||||
}
|
||||
}
|
||||
|
||||
if ($canDo->get('core.admin') || $canDo->get('core.options')) {
|
||||
$toolbar->preferences('com_users');
|
||||
}
|
||||
|
||||
$toolbar->help('Users');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user