[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |