[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/administrator/components/com_users/src/Controller/ -> MethodController.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Administrator
   5   * @subpackage  com_users
   6   *
   7   * @copyright   (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license     GNU General Public License version 2 or later; see LICENSE.txt
   9   */
  10  
  11  namespace Joomla\Component\Users\Administrator\Controller;
  12  
  13  use Exception;
  14  use Joomla\CMS\Application\CMSApplication;
  15  use Joomla\CMS\Event\MultiFactor\NotifyActionLog;
  16  use Joomla\CMS\Event\MultiFactor\SaveSetup;
  17  use Joomla\CMS\Factory;
  18  use Joomla\CMS\Language\Text;
  19  use Joomla\CMS\MVC\Controller\BaseController as BaseControllerAlias;
  20  use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
  21  use Joomla\CMS\Router\Route;
  22  use Joomla\CMS\User\User;
  23  use Joomla\CMS\User\UserFactoryInterface;
  24  use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
  25  use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
  26  use Joomla\Component\Users\Administrator\Model\MethodModel;
  27  use Joomla\Component\Users\Administrator\Table\MfaTable;
  28  use Joomla\Input\Input;
  29  use RuntimeException;
  30  
  31  // phpcs:disable PSR1.Files.SideEffects
  32  \defined('_JEXEC') or die;
  33  // phpcs:enable PSR1.Files.SideEffects
  34  
  35  /**
  36   * Multi-factor Authentication method controller
  37   *
  38   * @since 4.2.0
  39   */
  40  class MethodController extends BaseControllerAlias
  41  {
  42      /**
  43       * Public constructor
  44       *
  45       * @param   array                     $config   Plugin configuration
  46       * @param   MVCFactoryInterface|null  $factory  MVC Factory for the com_users component
  47       * @param   CMSApplication|null       $app      CMS application object
  48       * @param   Input|null                $input    Joomla CMS input object
  49       *
  50       * @since 4.2.0
  51       */
  52      public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null)
  53      {
  54          // We have to tell Joomla what is the name of the view, otherwise it defaults to the name of the *component*.
  55          $config['default_view'] = 'method';
  56          $config['default_task'] = 'add';
  57  
  58          parent::__construct($config, $factory, $app, $input);
  59      }
  60  
  61      /**
  62       * Execute a task by triggering a Method in the derived class.
  63       *
  64       * @param   string  $task    The task to perform. If no matching task is found, the '__default' task is executed, if
  65       *                           defined.
  66       *
  67       * @return  mixed   The value returned by the called Method.
  68       *
  69       * @throws  Exception
  70       * @since   4.2.0
  71       */
  72      public function execute($task)
  73      {
  74          if (empty($task) || $task === 'display') {
  75              $task = 'add';
  76          }
  77  
  78          return parent::execute($task);
  79      }
  80  
  81      /**
  82       * Add a new MFA Method
  83       *
  84       * @param   boolean        $cachable   Ignored. This page is never cached.
  85       * @param   boolean|array  $urlparams  Ignored. This page is never cached.
  86       *
  87       * @return  void
  88       * @throws  Exception
  89       * @since   4.2.0
  90       */
  91      public function add($cachable = false, $urlparams = []): void
  92      {
  93          $this->assertLoggedInUser();
  94  
  95          // Make sure I am allowed to edit the specified user
  96          $userId = $this->input->getInt('user_id', null);
  97          $user   = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId);
  98  
  99          $this->assertCanEdit($user);
 100  
 101          // Also make sure the Method really does exist
 102          $method = $this->input->getCmd('method');
 103          $this->assertMethodExists($method);
 104  
 105          /** @var MethodModel $model */
 106          $model = $this->getModel('Method');
 107          $model->setState('method', $method);
 108  
 109          // Pass the return URL to the view
 110          $returnURL  = $this->input->getBase64('returnurl');
 111          $viewLayout = $this->input->get('layout', 'default', 'string');
 112          $view       = $this->getView('Method', 'html');
 113          $view->setLayout($viewLayout);
 114          $view->returnURL = $returnURL;
 115          $view->user      = $user;
 116          $view->document  = $this->app->getDocument();
 117  
 118          $view->setModel($model, true);
 119  
 120          $event = new NotifyActionLog('onComUsersControllerMethodBeforeAdd', [$user, $method]);
 121          $this->app->getDispatcher()->dispatch($event->getName(), $event);
 122  
 123          $view->display();
 124      }
 125  
 126      /**
 127       * Edit an existing MFA Method
 128       *
 129       * @param   boolean        $cachable   Ignored. This page is never cached.
 130       * @param   boolean|array  $urlparams  Ignored. This page is never cached.
 131       *
 132       * @return  void
 133       * @throws  Exception
 134       * @since   4.2.0
 135       */
 136      public function edit($cachable = false, $urlparams = []): void
 137      {
 138          $this->assertLoggedInUser();
 139  
 140          // Make sure I am allowed to edit the specified user
 141          $userId = $this->input->getInt('user_id', null);
 142          $user   = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId);
 143  
 144          $this->assertCanEdit($user);
 145  
 146          // Also make sure the Method really does exist
 147          $id     = $this->input->getInt('id');
 148          $record = $this->assertValidRecordId($id, $user);
 149  
 150          if ($id <= 0) {
 151              throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
 152          }
 153  
 154          /** @var MethodModel $model */
 155          $model = $this->getModel('Method');
 156          $model->setState('id', $id);
 157  
 158          // Pass the return URL to the view
 159          $returnURL  = $this->input->getBase64('returnurl');
 160          $viewLayout = $this->input->get('layout', 'default', 'string');
 161          $view       = $this->getView('Method', 'html');
 162          $view->setLayout($viewLayout);
 163          $view->returnURL = $returnURL;
 164          $view->user      = $user;
 165          $view->document  = $this->app->getDocument();
 166  
 167          $view->setModel($model, true);
 168  
 169          $event = new NotifyActionLog('onComUsersControllerMethodBeforeEdit', [$id, $user]);
 170          $this->app->getDispatcher()->dispatch($event->getName(), $event);
 171  
 172          $view->display();
 173      }
 174  
 175      /**
 176       * Regenerate backup codes
 177       *
 178       * @param   boolean        $cachable   Ignored. This page is never cached.
 179       * @param   boolean|array  $urlparams  Ignored. This page is never cached.
 180       *
 181       * @return  void
 182       * @throws Exception
 183       * @since   4.2.0
 184       */
 185      public function regenerateBackupCodes($cachable = false, $urlparams = []): void
 186      {
 187          $this->assertLoggedInUser();
 188  
 189          $this->checkToken($this->input->getMethod());
 190  
 191          // Make sure I am allowed to edit the specified user
 192          $userId = $this->input->getInt('user_id', null);
 193          $user   = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId);
 194          $this->assertCanEdit($user);
 195  
 196          /** @var BackupcodesModel $model */
 197          $model = $this->getModel('Backupcodes');
 198          $model->regenerateBackupCodes($user);
 199  
 200          $backupCodesRecord = $model->getBackupCodesRecord($user);
 201  
 202          // Redirect
 203          $redirectUrl = 'index.php?option=com_users&task=method.edit&user_id=' . $userId . '&id=' . $backupCodesRecord->id;
 204          $returnURL   = $this->input->getBase64('returnurl');
 205  
 206          if (!empty($returnURL)) {
 207              $redirectUrl .= '&returnurl=' . $returnURL;
 208          }
 209  
 210          $this->setRedirect(Route::_($redirectUrl, false));
 211  
 212          $event = new NotifyActionLog('onComUsersControllerMethodAfterRegenerateBackupCodes');
 213          $this->app->getDispatcher()->dispatch($event->getName(), $event);
 214      }
 215  
 216      /**
 217       * Delete an existing MFA Method
 218       *
 219       * @param   boolean        $cachable   Ignored. This page is never cached.
 220       * @param   boolean|array  $urlparams  Ignored. This page is never cached.
 221       *
 222       * @return  void
 223       * @since   4.2.0
 224       */
 225      public function delete($cachable = false, $urlparams = []): void
 226      {
 227          $this->assertLoggedInUser();
 228  
 229          $this->checkToken($this->input->getMethod());
 230  
 231          // Make sure I am allowed to edit the specified user
 232          $userId = $this->input->getInt('user_id', null);
 233          $user    = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId);
 234          $this->assertCanDelete($user);
 235  
 236          // Also make sure the Method really does exist
 237          $id     = $this->input->getInt('id');
 238          $record = $this->assertValidRecordId($id, $user);
 239  
 240          if ($id <= 0) {
 241              throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
 242          }
 243  
 244          $type    = null;
 245          $message = null;
 246  
 247          $event = new NotifyActionLog('onComUsersControllerMethodBeforeDelete', [$id, $user]);
 248          $this->app->getDispatcher()->dispatch($event->getName(), $event);
 249  
 250          try {
 251              $record->delete();
 252          } catch (Exception $e) {
 253              $message = $e->getMessage();
 254              $type    = 'error';
 255          }
 256  
 257          // Redirect
 258          $url       = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false);
 259          $returnURL = $this->input->getBase64('returnurl');
 260  
 261          if (!empty($returnURL)) {
 262              $url = base64_decode($returnURL);
 263          }
 264  
 265          $this->setRedirect($url, $message, $type);
 266      }
 267  
 268      /**
 269       * Save the MFA Method
 270       *
 271       * @param   boolean        $cachable   Ignored. This page is never cached.
 272       * @param   boolean|array  $urlparams  Ignored. This page is never cached.
 273       *
 274       * @return  void
 275       * @since   4.2.0
 276       */
 277      public function save($cachable = false, $urlparams = []): void
 278      {
 279          $this->assertLoggedInUser();
 280  
 281          $this->checkToken($this->input->getMethod());
 282  
 283          // Make sure I am allowed to edit the specified user
 284          $userId = $this->input->getInt('user_id', null);
 285          $user   = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId);
 286          $this->assertCanEdit($user);
 287  
 288          // Redirect
 289          $url       = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false);
 290          $returnURL = $this->input->getBase64('returnurl');
 291  
 292          if (!empty($returnURL)) {
 293              $url = base64_decode($returnURL);
 294          }
 295  
 296          // The record must either be new (ID zero) or exist
 297          $id     = $this->input->getInt('id', 0);
 298          $record = $this->assertValidRecordId($id, $user);
 299  
 300          // If it's a new record we need to read the Method from the request and update the (not yet created) record.
 301          if ($record->id == 0) {
 302              $methodName = $this->input->getCmd('method');
 303              $this->assertMethodExists($methodName);
 304              $record->method = $methodName;
 305          }
 306  
 307          /** @var MethodModel $model */
 308          $model = $this->getModel('Method');
 309  
 310          // Ask the plugin to validate the input by calling onUserMultifactorSaveSetup
 311          $result = [];
 312          $input  = $this->app->input;
 313  
 314          $event = new NotifyActionLog('onComUsersControllerMethodBeforeSave', [$id, $user]);
 315          $this->app->getDispatcher()->dispatch($event->getName(), $event);
 316  
 317          try {
 318              $event         = new SaveSetup($record, $input);
 319              $pluginResults = $this->app
 320                  ->getDispatcher()
 321                  ->dispatch($event->getName(), $event)
 322                  ->getArgument('result', []);
 323  
 324              foreach ($pluginResults as $pluginResult) {
 325                  $result = array_merge($result, $pluginResult);
 326              }
 327          } catch (RuntimeException $e) {
 328              // Go back to the edit page
 329              $nonSefUrl = 'index.php?option=com_users&task=method.';
 330  
 331              if ($id) {
 332                  $nonSefUrl .= 'edit&id=' . (int) $id;
 333              } else {
 334                  $nonSefUrl .= 'add&method=' . $record->method;
 335              }
 336  
 337              $nonSefUrl .= '&user_id=' . $userId;
 338  
 339              if (!empty($returnURL)) {
 340                  $nonSefUrl .= '&returnurl=' . urlencode($returnURL);
 341              }
 342  
 343              $url = Route::_($nonSefUrl, false);
 344              $this->setRedirect($url, $e->getMessage(), 'error');
 345  
 346              return;
 347          }
 348  
 349          // Update the record's options with the plugin response
 350          $title = $this->input->getString('title', null);
 351          $title = trim($title);
 352  
 353          if (empty($title)) {
 354              $method = $model->getMethod($record->method);
 355              $title  = $method['display'];
 356          }
 357  
 358          // Update the record's "default" flag
 359          $default         = $this->input->getBool('default', false);
 360          $record->title   = $title;
 361          $record->options = $result;
 362          $record->default = $default ? 1 : 0;
 363  
 364          // Ask the model to save the record
 365          $saved = $record->store();
 366  
 367          if (!$saved) {
 368              // Go back to the edit page
 369              $nonSefUrl = 'index.php?option=com_users&task=method.';
 370  
 371              if ($id) {
 372                  $nonSefUrl .= 'edit&id=' . (int) $id;
 373              } else {
 374                  $nonSefUrl .= 'add';
 375              }
 376  
 377              $nonSefUrl .= '&user_id=' . $userId;
 378  
 379              if (!empty($returnURL)) {
 380                  $nonSefUrl .= '&returnurl=' . urlencode($returnURL);
 381              }
 382  
 383              $url = Route::_($nonSefUrl, false);
 384              $this->setRedirect($url, $record->getError(), 'error');
 385  
 386              return;
 387          }
 388  
 389          $this->setRedirect($url);
 390      }
 391  
 392      /**
 393       * Assert that the provided ID is a valid record identified for the given user
 394       *
 395       * @param   int        $id    Record ID to check
 396       * @param   User|null  $user  User record. Null to use current user.
 397       *
 398       * @return  MfaTable  The loaded record
 399       * @since   4.2.0
 400       */
 401      private function assertValidRecordId($id, ?User $user = null): MfaTable
 402      {
 403          if (is_null($user)) {
 404              $user = $this->app->getIdentity()
 405                  ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
 406          }
 407  
 408          /** @var MethodModel $model */
 409          $model = $this->getModel('Method');
 410  
 411          $model->setState('id', $id);
 412  
 413          $record = $model->getRecord($user);
 414  
 415          if (is_null($record) || ($record->id != $id) || ($record->user_id != $user->id)) {
 416              throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
 417          }
 418  
 419          return $record;
 420      }
 421  
 422      /**
 423       * Assert that the user can add / edit MFA methods.
 424       *
 425       * @param   User|null  $user  User record. Null to use current user.
 426       *
 427       * @return  void
 428       * @throws  RuntimeException|Exception
 429       * @since   4.2.0
 430       */
 431      private function assertCanEdit(?User $user = null): void
 432      {
 433          if (!MfaHelper::canAddEditMethod($user)) {
 434              throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
 435          }
 436      }
 437  
 438      /**
 439       * Assert that the user can delete MFA records / disable MFA.
 440       *
 441       * @param   User|null  $user  User record. Null to use current user.
 442       *
 443       * @return  void
 444       * @throws  RuntimeException|Exception
 445       * @since   4.2.0
 446       */
 447      private function assertCanDelete(?User $user = null): void
 448      {
 449          if (!MfaHelper::canDeleteMethod($user)) {
 450              throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
 451          }
 452      }
 453  
 454      /**
 455       * Assert that the specified MFA Method exists, is activated and enabled for the current user
 456       *
 457       * @param   string|null  $method  The Method to check
 458       *
 459       * @return  void
 460       * @since   4.2.0
 461       */
 462      private function assertMethodExists(?string $method): void
 463      {
 464          /** @var MethodModel $model */
 465          $model = $this->getModel('Method');
 466  
 467          if (empty($method) || !$model->methodExists($method)) {
 468              throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
 469          }
 470      }
 471  
 472      /**
 473       * Assert that there is a logged in user.
 474       *
 475       * @return  void
 476       * @since   4.2.0
 477       */
 478      private function assertLoggedInUser(): void
 479      {
 480          $user = $this->app->getIdentity()
 481              ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
 482  
 483          if ($user->guest) {
 484              throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
 485          }
 486      }
 487  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer