[ 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\Controller; 12 13 use Exception; 14 use Joomla\CMS\Application\CMSApplication; 15 use Joomla\CMS\Date\Date; 16 use Joomla\CMS\Event\MultiFactor\NotifyActionLog; 17 use Joomla\CMS\Event\MultiFactor\Validate; 18 use Joomla\CMS\Factory; 19 use Joomla\CMS\Language\Text; 20 use Joomla\CMS\MVC\Controller\BaseController; 21 use Joomla\CMS\MVC\Factory\MVCFactoryInterface; 22 use Joomla\CMS\Router\Route; 23 use Joomla\CMS\Uri\Uri; 24 use Joomla\CMS\User\UserFactoryInterface; 25 use Joomla\Component\Users\Administrator\Model\BackupcodesModel; 26 use Joomla\Component\Users\Administrator\Model\CaptiveModel; 27 use Joomla\Input\Input; 28 use RuntimeException; 29 30 // phpcs:disable PSR1.Files.SideEffects 31 \defined('_JEXEC') or die; 32 // phpcs:enable PSR1.Files.SideEffects 33 34 /** 35 * Captive Multi-factor Authentication page controller 36 * 37 * @since 4.2.0 38 */ 39 class CaptiveController extends BaseController 40 { 41 /** 42 * Public constructor 43 * 44 * @param array $config Plugin configuration 45 * @param MVCFactoryInterface|null $factory MVC Factory for the com_users component 46 * @param CMSApplication|null $app CMS application object 47 * @param Input|null $input Joomla CMS input object 48 * 49 * @since 4.2.0 50 */ 51 public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null) 52 { 53 parent::__construct($config, $factory, $app, $input); 54 55 $this->registerTask('captive', 'display'); 56 } 57 58 /** 59 * Displays the captive login page 60 * 61 * @param boolean $cachable Ignored. This page is never cached. 62 * @param boolean|array $urlparams Ignored. This page is never cached. 63 * 64 * @return void 65 * @throws Exception 66 * @since 4.2.0 67 */ 68 public function display($cachable = false, $urlparams = false): void 69 { 70 $user = $this->app->getIdentity() 71 ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); 72 73 // Only allow logged in Users 74 if ($user->guest) { 75 throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); 76 } 77 78 // Get the view object 79 $viewLayout = $this->input->get('layout', 'default', 'string'); 80 $view = $this->getView( 81 'Captive', 82 'html', 83 '', 84 [ 85 'base_path' => $this->basePath, 86 'layout' => $viewLayout, 87 ] 88 ); 89 90 $view->document = $this->app->getDocument(); 91 92 // If we're already logged in go to the site's home page 93 if ((int) $this->app->getSession()->get('com_users.mfa_checked', 0) === 1) { 94 $url = Route::_('index.php?option=com_users&task=methods.display', false); 95 96 $this->setRedirect($url); 97 } 98 99 // Pass the model to the view 100 /** @var CaptiveModel $model */ 101 $model = $this->getModel('Captive'); 102 $view->setModel($model, true); 103 104 /** @var BackupcodesModel $codesModel */ 105 $codesModel = $this->getModel('Backupcodes'); 106 $view->setModel($codesModel, false); 107 108 try { 109 // Suppress all modules on the page except those explicitly allowed 110 $model->suppressAllModules(); 111 } catch (Exception $e) { 112 // If we can't kill the modules we can still survive. 113 } 114 115 // Pass the MFA record ID to the model 116 $recordId = $this->input->getInt('record_id', null); 117 $model->setState('record_id', $recordId); 118 119 // Do not go through $this->display() because it overrides the model. 120 $view->display(); 121 } 122 123 /** 124 * Validate the MFA code entered by the user 125 * 126 * @param bool $cachable Ignored. This page is never cached. 127 * @param array $urlparameters Ignored. This page is never cached. 128 * 129 * @return void 130 * @throws Exception 131 * @since 4.2.0 132 */ 133 public function validate($cachable = false, $urlparameters = []) 134 { 135 // CSRF Check 136 $this->checkToken($this->input->getMethod()); 137 138 // Get the MFA parameters from the request 139 $recordId = $this->input->getInt('record_id', null); 140 $code = $this->input->get('code', null, 'raw'); 141 /** @var CaptiveModel $model */ 142 $model = $this->getModel('Captive'); 143 144 // Validate the MFA record 145 $model->setState('record_id', $recordId); 146 $record = $model->getRecord(); 147 148 if (empty($record)) { 149 $event = new NotifyActionLog('onComUsersCaptiveValidateInvalidMethod'); 150 $this->app->getDispatcher()->dispatch($event->getName(), $event); 151 152 throw new RuntimeException(Text::_('COM_USERS_MFA_INVALID_METHOD'), 500); 153 } 154 155 // Validate the code 156 $user = $this->app->getIdentity() 157 ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); 158 159 $event = new Validate($record, $user, $code); 160 $results = $this->app 161 ->getDispatcher() 162 ->dispatch($event->getName(), $event) 163 ->getArgument('result', []); 164 165 $isValidCode = false; 166 167 if ($record->method === 'backupcodes') { 168 /** @var BackupcodesModel $codesModel */ 169 $codesModel = $this->getModel('Backupcodes'); 170 $results = [$codesModel->isBackupCode($code, $user)]; 171 /** 172 * This is required! Do not remove! 173 * 174 * There is a store() call below. It saves the in-memory MFA record to the database. That includes the 175 * options key which contains the configuration of the Method. For backup codes, these are the actual codes 176 * you can use. When we check for a backup code validity we also "burn" it, i.e. we remove it from the 177 * options table and save that to the database. However, this DOES NOT update the $record here. Therefore 178 * the call to saveRecord() would overwrite the database contents with a record that _includes_ the backup 179 * code we had just burned. As a result the single use backup codes end up being multiple use. 180 * 181 * By doing a getRecord() here, right after we have "burned" any correct backup codes, we resolve this 182 * issue. The loaded record will reflect the database contents where the options DO NOT include the code we 183 * just used. Therefore the call to store() will result in the correct database state, i.e. the used backup 184 * code being removed. 185 */ 186 $record = $model->getRecord(); 187 } 188 189 $isValidCode = array_reduce( 190 $results, 191 function (bool $carry, $result) { 192 return $carry || boolval($result); 193 }, 194 false 195 ); 196 197 if (!$isValidCode) { 198 // The code is wrong. Display an error and go back. 199 $captiveURL = Route::_('index.php?option=com_users&view=captive&record_id=' . $recordId, false); 200 $message = Text::_('COM_USERS_MFA_INVALID_CODE'); 201 $this->setRedirect($captiveURL, $message, 'error'); 202 203 $event = new NotifyActionLog('onComUsersCaptiveValidateFailed', [$record->title]); 204 $this->app->getDispatcher()->dispatch($event->getName(), $event); 205 206 return; 207 } 208 209 // Update the Last Used, UA and IP columns 210 $jNow = Date::getInstance(); 211 212 $record->last_used = $jNow->toSql(); 213 $record->store(); 214 215 // Flag the user as fully logged in 216 $session = $this->app->getSession(); 217 $session->set('com_users.mfa_checked', 1); 218 $session->set('com_users.mandatory_mfa_setup', 0); 219 220 // Get the return URL stored by the plugin in the session 221 $returnUrl = $session->get('com_users.return_url', ''); 222 223 // If the return URL is not set or not internal to this site redirect to the site's front page 224 if (empty($returnUrl) || !Uri::isInternal($returnUrl)) { 225 $returnUrl = Uri::base(); 226 } 227 228 $this->setRedirect($returnUrl); 229 230 $event = new NotifyActionLog('onComUsersCaptiveValidateSuccess', [$record->title]); 231 $this->app->getDispatcher()->dispatch($event->getName(), $event); 232 } 233 }
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 |