[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/administrator/components/com_users/src/Model/ -> CaptiveModel.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\Model;
  12  
  13  use Exception;
  14  use Joomla\CMS\Application\CMSApplication;
  15  use Joomla\CMS\Component\ComponentHelper;
  16  use Joomla\CMS\Event\MultiFactor\Captive;
  17  use Joomla\CMS\Factory;
  18  use Joomla\CMS\Language\Text;
  19  use Joomla\CMS\MVC\Model\BaseDatabaseModel;
  20  use Joomla\CMS\User\User;
  21  use Joomla\CMS\User\UserFactoryInterface;
  22  use Joomla\Component\Users\Administrator\DataShape\CaptiveRenderOptions;
  23  use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
  24  use Joomla\Component\Users\Administrator\Table\MfaTable;
  25  use Joomla\Event\Event;
  26  
  27  // phpcs:disable PSR1.Files.SideEffects
  28  \defined('_JEXEC') or die;
  29  // phpcs:enable PSR1.Files.SideEffects
  30  
  31  /**
  32   * Captive Multi-factor Authentication page's model
  33   *
  34   * @since 4.2.0
  35   */
  36  class CaptiveModel extends BaseDatabaseModel
  37  {
  38      /**
  39       * Cache of the names of the currently active MFA Methods
  40       *
  41       * @var  array|null
  42       * @since 4.2.0
  43       */
  44      protected $activeMFAMethodNames = null;
  45  
  46      /**
  47       * Prevents Joomla from displaying any modules.
  48       *
  49       * This is implemented with a trick. If you use jdoc tags to load modules the JDocumentRendererHtmlModules
  50       * uses JModuleHelper::getModules() to load the list of modules to render. This goes through JModuleHelper::load()
  51       * which triggers the onAfterModuleList event after cleaning up the module list from duplicates. By resetting
  52       * the list to an empty array we force Joomla to not display any modules.
  53       *
  54       * Similar code paths are followed by any canonical code which tries to load modules. So even if your template does
  55       * not use jdoc tags this code will still work as expected.
  56       *
  57       * @param   CMSApplication|null  $app  The CMS application to manipulate
  58       *
  59       * @return  void
  60       * @throws  Exception
  61       *
  62       * @since 4.2.0
  63       */
  64      public function suppressAllModules(CMSApplication $app = null): void
  65      {
  66          if (is_null($app)) {
  67              $app = Factory::getApplication();
  68          }
  69  
  70          $app->registerEvent('onAfterModuleList', [$this, 'onAfterModuleList']);
  71      }
  72  
  73      /**
  74       * Get the MFA records for the user which correspond to active plugins
  75       *
  76       * @param   User|null  $user                The user for which to fetch records. Skip to use the current user.
  77       * @param   bool       $includeBackupCodes  Should I include the backup codes record?
  78       *
  79       * @return  array
  80       * @throws  Exception
  81       *
  82       * @since 4.2.0
  83       */
  84      public function getRecords(User $user = null, bool $includeBackupCodes = false): array
  85      {
  86          if (is_null($user)) {
  87              $user = Factory::getApplication()->getIdentity()
  88                  ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
  89          }
  90  
  91          // Get the user's MFA records
  92          $records = MfaHelper::getUserMfaRecords($user->id);
  93  
  94          // No MFA Methods? Then we obviously don't need to display a Captive login page.
  95          if (empty($records)) {
  96              return [];
  97          }
  98  
  99          // Get the enabled MFA Methods' names
 100          $methodNames = $this->getActiveMethodNames();
 101  
 102          // Filter the records based on currently active MFA Methods
 103          $ret = [];
 104  
 105          $methodNames[] = 'backupcodes';
 106          $methodNames   = array_unique($methodNames);
 107  
 108          if (!$includeBackupCodes) {
 109              $methodNames = array_filter(
 110                  $methodNames,
 111                  function ($method) {
 112                      return $method != 'backupcodes';
 113                  }
 114              );
 115          }
 116  
 117          foreach ($records as $record) {
 118              // Backup codes must not be included in the list. We add them in the View, at the end of the list.
 119              if (in_array($record->method, $methodNames)) {
 120                  $ret[$record->id] = $record;
 121              }
 122          }
 123  
 124          return $ret;
 125      }
 126  
 127      /**
 128       * Return all the active MFA Methods' names
 129       *
 130       * @return  array
 131       * @since 4.2.0
 132       */
 133      private function getActiveMethodNames(): ?array
 134      {
 135          if (!is_null($this->activeMFAMethodNames)) {
 136              return $this->activeMFAMethodNames;
 137          }
 138  
 139          // Let's get a list of all currently active MFA Methods
 140          $mfaMethods = MfaHelper::getMfaMethods();
 141  
 142          // If no MFA Method is active we can't really display a Captive login page.
 143          if (empty($mfaMethods)) {
 144              $this->activeMFAMethodNames = [];
 145  
 146              return $this->activeMFAMethodNames;
 147          }
 148  
 149          // Get a list of just the Method names
 150          $this->activeMFAMethodNames = [];
 151  
 152          foreach ($mfaMethods as $mfaMethod) {
 153              $this->activeMFAMethodNames[] = $mfaMethod['name'];
 154          }
 155  
 156          return $this->activeMFAMethodNames;
 157      }
 158  
 159      /**
 160       * Get the currently selected MFA record for the current user. If the record ID is empty, it does not correspond to
 161       * the currently logged in user or does not correspond to an active plugin null is returned instead.
 162       *
 163       * @param   User|null  $user  The user for which to fetch records. Skip to use the current user.
 164       *
 165       * @return  MfaTable|null
 166       * @throws  Exception
 167       *
 168       * @since 4.2.0
 169       */
 170      public function getRecord(?User $user = null): ?MfaTable
 171      {
 172          $id = (int) $this->getState('record_id', null);
 173  
 174          if ($id <= 0) {
 175              return null;
 176          }
 177  
 178          if (is_null($user)) {
 179              $user = Factory::getApplication()->getIdentity()
 180                  ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
 181          }
 182  
 183          /** @var MfaTable $record */
 184          $record = $this->getTable('Mfa', 'Administrator');
 185          $loaded = $record->load(
 186              [
 187                  'user_id' => $user->id,
 188                  'id'      => $id,
 189              ]
 190          );
 191  
 192          if (!$loaded) {
 193              return null;
 194          }
 195  
 196          $methodNames = $this->getActiveMethodNames();
 197  
 198          if (!in_array($record->method, $methodNames) && ($record->method != 'backupcodes')) {
 199              return null;
 200          }
 201  
 202          return $record;
 203      }
 204  
 205      /**
 206       * Load the Captive login page render options for a specific MFA record
 207       *
 208       * @param   MfaTable  $record  The MFA record to process
 209       *
 210       * @return  CaptiveRenderOptions  The rendering options
 211       * @since 4.2.0
 212       */
 213      public function loadCaptiveRenderOptions(?MfaTable $record): CaptiveRenderOptions
 214      {
 215          $renderOptions = new CaptiveRenderOptions();
 216  
 217          if (empty($record)) {
 218              return $renderOptions;
 219          }
 220  
 221          $event   = new Captive($record);
 222          $results = Factory::getApplication()
 223              ->getDispatcher()
 224              ->dispatch($event->getName(), $event)
 225              ->getArgument('result', []);
 226  
 227          if (empty($results)) {
 228              if ($record->method === 'backupcodes') {
 229                  return $renderOptions->merge(
 230                      [
 231                          'pre_message' => Text::_('COM_USERS_USER_BACKUPCODES_CAPTIVE_PROMPT'),
 232                          'input_type' => 'number',
 233                          'label' => Text::_('COM_USERS_USER_BACKUPCODE'),
 234                      ]
 235                  );
 236              }
 237  
 238              return $renderOptions;
 239          }
 240  
 241          foreach ($results as $result) {
 242              if (empty($result)) {
 243                  continue;
 244              }
 245  
 246              return $renderOptions->merge($result);
 247          }
 248  
 249          return $renderOptions;
 250      }
 251  
 252      /**
 253       * Returns the title to display in the Captive login page, or an empty string if no title is to be displayed.
 254       *
 255       * @return  string
 256       * @since 4.2.0
 257       */
 258      public function getPageTitle(): string
 259      {
 260          // In the frontend we can choose if we will display a title
 261          $showTitle = (bool) ComponentHelper::getParams('com_users')
 262              ->get('frontend_show_title', 1);
 263  
 264          if (!$showTitle) {
 265              return '';
 266          }
 267  
 268          return Text::_('COM_USERS_USER_MULTIFACTOR_AUTH');
 269      }
 270  
 271      /**
 272       * Translate a MFA Method's name into its human-readable, display name
 273       *
 274       * @param   string  $name  The internal MFA Method name
 275       *
 276       * @return  string
 277       * @since 4.2.0
 278       */
 279      public function translateMethodName(string $name): string
 280      {
 281          static $map = null;
 282  
 283          if (!is_array($map)) {
 284              $map        = [];
 285              $mfaMethods = MfaHelper::getMfaMethods();
 286  
 287              if (!empty($mfaMethods)) {
 288                  foreach ($mfaMethods as $mfaMethod) {
 289                      $map[$mfaMethod['name']] = $mfaMethod['display'];
 290                  }
 291              }
 292          }
 293  
 294          if ($name == 'backupcodes') {
 295              return Text::_('COM_USERS_USER_BACKUPCODES');
 296          }
 297  
 298          return $map[$name] ?? $name;
 299      }
 300  
 301      /**
 302       * Translate a MFA Method's name into the relative URL if its logo image
 303       *
 304       * @param   string  $name  The internal MFA Method name
 305       *
 306       * @return  string
 307       * @since 4.2.0
 308       */
 309      public function getMethodImage(string $name): string
 310      {
 311          static $map = null;
 312  
 313          if (!is_array($map)) {
 314              $map        = [];
 315              $mfaMethods = MfaHelper::getMfaMethods();
 316  
 317              if (!empty($mfaMethods)) {
 318                  foreach ($mfaMethods as $mfaMethod) {
 319                      $map[$mfaMethod['name']] = $mfaMethod['image'];
 320                  }
 321              }
 322          }
 323  
 324          if ($name == 'backupcodes') {
 325              return 'media/com_users/images/emergency.svg';
 326          }
 327  
 328          return $map[$name] ?? $name;
 329      }
 330  
 331      /**
 332       * Process the modules list on Joomla! 4.
 333       *
 334       * Joomla! 4.x is passing an Event object. The first argument of the event object is the array of modules. After
 335       * filtering it we have to overwrite the event argument (NOT just return the new list of modules). If a future
 336       * version of Joomla! uses immutable events we'll have to use Reflection to do that or Joomla! would have to fix
 337       * the way this event is handled, taking its return into account. For now, we just abuse the mutable event
 338       * properties - a feature of the event objects we discussed in the Joomla! 4 Working Group back in August 2015.
 339       *
 340       * @param   Event  $event  The Joomla! event object
 341       *
 342       * @return  void
 343       * @throws  Exception
 344       *
 345       * @since 4.2.0
 346       */
 347      public function onAfterModuleList(Event $event): void
 348      {
 349          $modules = $event->getArgument(0);
 350  
 351          if (empty($modules)) {
 352              return;
 353          }
 354  
 355          $this->filterModules($modules);
 356  
 357          $event->setArgument(0, $modules);
 358      }
 359  
 360      /**
 361       * This is the Method which actually filters the sites modules based on the allowed module positions specified by
 362       * the user.
 363       *
 364       * @param   array  $modules  The list of the site's modules. Passed by reference.
 365       *
 366       * @return  void  The by-reference value is modified instead.
 367       * @since 4.2.0
 368       * @throws  Exception
 369       */
 370      private function filterModules(array &$modules): void
 371      {
 372          $allowedPositions = $this->getAllowedModulePositions();
 373  
 374          if (empty($allowedPositions)) {
 375              $modules = [];
 376  
 377              return;
 378          }
 379  
 380          $filtered = [];
 381  
 382          foreach ($modules as $module) {
 383              if (in_array($module->position, $allowedPositions)) {
 384                  $filtered[] = $module;
 385              }
 386          }
 387  
 388          $modules = $filtered;
 389      }
 390  
 391      /**
 392       * Get a list of module positions we are allowed to display
 393       *
 394       * @return  array
 395       * @throws  Exception
 396       *
 397       * @since 4.2.0
 398       */
 399      private function getAllowedModulePositions(): array
 400      {
 401          $isAdmin = Factory::getApplication()->isClient('administrator');
 402  
 403          // Load the list of allowed module positions from the component's settings. May be different for front- and back-end
 404          $configKey = 'allowed_positions_' . ($isAdmin ? 'backend' : 'frontend');
 405          $res       = ComponentHelper::getParams('com_users')->get($configKey, []);
 406  
 407          // In the backend we must always add the 'title' module position
 408          if ($isAdmin) {
 409              $res[] = 'title';
 410              $res[] = 'toolbar';
 411          }
 412  
 413          return $res;
 414      }
 415  }


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