1<?php 2// see: https://scrutinizer-ci.com/g/LimeSurvey/LimeSurvey/issues/master/files/application/controllers/admin/authentication.php?selectedSeverities[0]=10&orderField=path&order=asc&honorSelectedPaths=0 3// use LimeSurvey\PluginManager\PluginEvent; 4 5if (!defined('BASEPATH')) { 6 exit('No direct script access allowed'); 7} 8/* 9* LimeSurvey 10* Copyright (C) 2007-2011 The LimeSurvey Project Team / Carsten Schmitz 11* All rights reserved. 12* License: GNU/GPL License v2 or later, see LICENSE.php 13* LimeSurvey is free software. This version may have been modified pursuant 14* to the GNU General Public License, and as distributed it includes or 15* is derivative of works licensed under the GNU General Public License or 16* other free or open source software licenses. 17* See COPYRIGHT.php for copyright notices and details. 18* 19*/ 20 21/** 22* Authentication Controller 23* 24* This controller performs authentication 25* 26* @package LimeSurvey 27* @subpackage Backend 28* 29* @method void redirect(string|array $url, boolean $terminate, integer $statusCode) 30 */ 31class Authentication extends Survey_Common_Action 32{ 33 34 /** 35 * Show login screen and parse login data 36 * Will redirect or echo json depending on ajax call 37 * This function is called while accessing the login page: index.php/admin/authentication/sa/login 38 */ 39 public function index() 40 { 41 /* Set adminlang to the one set in dropdown */ 42 if (Yii::app()->request->getParam('loginlang', 'default') != 'default') { 43 Yii::app()->session['adminlang'] = Yii::app()->request->getParam('loginlang', 'default'); 44 Yii::app()->setLanguage(Yii::app()->session["adminlang"]); 45 } 46 // The page should be shown only for non logged in users 47 $this->_redirectIfLoggedIn(); 48 49 // Result can be success, fail or data for template 50 $result = self::prepareLogin(); 51 52 $isAjax = isset($_GET['ajax']) && $_GET['ajax'] == 1; 53 $succeeded = isset($result[0]) && $result[0] == 'success'; 54 $failed = isset($result[0]) && $result[0] == 'failed'; 55 56 // If Ajax, echo success or failure json 57 if ($isAjax) { 58 Yii::import('application.helpers.admin.ajax_helper', true); 59 if ($succeeded) { 60 ls\ajax\AjaxHelper::outputSuccess(gT('Successful login')); 61 return; 62 } else if ($failed) { 63 ls\ajax\AjaxHelper::outputError(gT('Incorrect username and/or password!')); 64 return; 65 } 66 } 67 // If not ajax, redirect to admin startpage or again to login form 68 else { 69 if ($succeeded) { 70 self::doRedirect(); 71 } else if ($failed) { 72 $message = $result[1]; 73 App()->user->setFlash('error', $message); 74 App()->getController()->redirect(array('/admin/authentication/sa/login')); 75 } 76 } 77 78 // Neither success nor failure, meaning no form submission - result = template data from plugin 79 $aData = $result; 80 81 // If for any reason, the plugin bugs, we can't let the user with a blank screen. 82 $this->_renderWrappedTemplate('authentication', 'login', $aData); 83 } 84 85 /** 86 * Prepare login and return result 87 * It checks if the authdb plugin is registered and active 88 * @return array Either success, failure or plugin data (used in login form) 89 */ 90 public static function prepareLogin() 91 { 92 $aData = array(); 93 94 // Plugins, include core plugins, can't be activated by default. 95 // So after a fresh installation, core plugins are not activated 96 // They need to be manually loaded. 97 if (!class_exists('Authdb', false)) { 98 $plugin = Plugin::model()->findByAttributes(array('name'=>'Authdb')); 99 if (!$plugin) { 100 $plugin = new Plugin(); 101 $plugin->name = 'Authdb'; 102 $plugin->active = 1; 103 $plugin->save(); 104 App()->getPluginManager()->loadPlugin('Authdb', $plugin->id); 105 } else { 106 $plugin->active = 1; 107 $plugin->save(); 108 } 109 } 110 111 // In Authdb, the plugin event "beforeLogin" checks if the url param "onepass" is set 112 // if yes, it will call AuthPluginBase::setAuthPlugin to set to true the plugin private parameter "_stop", so the form will not be displayed 113 // @see: application/core/plugins/Authdb/Authdb.php: function beforeLogin() 114 $beforeLogin = new PluginEvent('beforeLogin'); 115 $beforeLogin->set('identity', new LSUserIdentity('', '')); 116 App()->getPluginManager()->dispatchEvent($beforeLogin); 117 118 /* @var $identity LSUserIdentity */ 119 $identity = $beforeLogin->get('identity'); // Why here? 120 121 // If the plugin private parameter "_stop" is false and the login form has not been submitted: render the login form 122 if (!$beforeLogin->isStopped() && is_null(App()->getRequest()->getPost('login_submit'))) { 123 // First step: set the value of $aData['defaultAuth'] 124 // This variable will be used to select the default value of the Authentication method selector 125 // which is shown only if there is more than one plugin auth on... 126 // @see application/views/admin/authentication/login.php 127 128 // First it checks if the current plugin force the authentication default value... 129 // NB: A plugin SHOULD NOT be able to over pass the configuration file 130 // @see: http://img.memecdn.com/knees-weak-arms-are-heavy_c_3011277.jpg 131 if (!is_null($beforeLogin->get('default'))) { 132 $aData['defaultAuth'] = $beforeLogin->get('default'); 133 } else { 134 // THen, it checks if the the user set a different default plugin auth in application/config/config.php 135 // eg: 'config'=>array()'debug'=>2,'debugsql'=>0, 'default_displayed_auth_method'=>'muh_auth_method') 136 if (App()->getPluginManager()->isPluginActive(Yii::app()->getConfig('default_displayed_auth_method'))) { 137 $aData['defaultAuth'] = Yii::app()->getConfig('default_displayed_auth_method'); 138 } else { 139 $aData['defaultAuth'] = 'Authdb'; 140 } 141 } 142 143 // Call the plugin method newLoginForm 144 // For Authdb: @see: application/core/plugins/Authdb/Authdb.php: function newLoginForm() 145 $newLoginForm = new PluginEvent('newLoginForm'); 146 App()->getPluginManager()->dispatchEvent($newLoginForm); // inject the HTML of the form inside the private varibale "_content" of the plugin 147 $aData['summary'] = self::getSummary('logout'); 148 $aData['pluginContent'] = $newLoginForm->getAllContent(); // Retreives the private varibale "_content" , and parse it to $aData['pluginContent'], which will be rendered in application/views/admin/authentication/login.php 149 } else { 150 // The form has been submited, or the plugin has been stoped (so normally, the value of login/password are available) 151 152 // Handle getting the post and populating the identity there 153 $authMethod = App()->getRequest()->getPost('authMethod', $identity->plugin); // If form has been submitted, $_POST['authMethod'] is set, else $identity->plugin should be set, ELSE: TODO error 154 $identity->plugin = $authMethod; 155 156 // Call the function afterLoginFormSubmit of the plugin. 157 // For Authdb, it calls AuthPluginBase::afterLoginFormSubmit() 158 // which set the plugin's private variables _username and _password with the POST informations if it's a POST request else it does nothing 159 $event = new PluginEvent('afterLoginFormSubmit'); 160 $event->set('identity', $identity); 161 App()->getPluginManager()->dispatchEvent($event, array($authMethod)); 162 $identity = $event->get('identity'); 163 164 // Now authenticate 165 // This call LSUserIdentity::authenticate() (application/core/LSUserIdentity.php)) 166 // which will call the plugin function newUserSession() (eg: Authdb::newUserSession() ) 167 // TODO: for sake of clarity, the plugin function should be renamed to authenticate(). 168 if ($identity->authenticate()) { 169 FailedLoginAttempt::model()->deleteAttempts(); 170 App()->user->setState('plugin', $authMethod); 171 172 Yii::app()->session['just_logged_in'] = true; 173 Yii::app()->session['loginsummary'] = self::getSummary(); 174 175 $event = new PluginEvent('afterSuccessfulLogin'); 176 App()->getPluginManager()->dispatchEvent($event); 177 178 return array('success'); 179 } else { 180 // Failed 181 $event = new PluginEvent('afterFailedLoginAttempt'); 182 $event->set('identity', $identity); 183 App()->getPluginManager()->dispatchEvent($event); 184 185 $message = $identity->errorMessage; 186 if (empty($message)) { 187 // If no message, return a default message 188 $message = gT('Incorrect username and/or password!'); 189 } 190 return array('failed', $message); 191 } 192 } 193 194 return $aData; 195 } 196 197 /** 198 * Logout user 199 * @return void 200 */ 201 public function logout() 202 { 203 /* Adding beforeLogout event */ 204 $beforeLogout = new PluginEvent('beforeLogout'); 205 App()->getPluginManager()->dispatchEvent($beforeLogout); 206 regenerateCSRFToken(); 207 App()->user->logout(); 208 App()->user->setFlash('loginmessage', gT('Logout successful.')); 209 210 /* Adding afterLogout event */ 211 $event = new PluginEvent('afterLogout'); 212 App()->getPluginManager()->dispatchEvent($event); 213 214 $this->getController()->redirect(array('/admin/authentication/sa/login')); 215 } 216 217 /** 218 * Forgot Password screen 219 * @return void 220 */ 221 public function forgotpassword() 222 { 223 $this->_redirectIfLoggedIn(); 224 225 if (!Yii::app()->request->getPost('action')) { 226 $this->_renderWrappedTemplate('authentication', 'forgotpassword'); 227 } else { 228 $sUserName = Yii::app()->request->getPost('user'); 229 $sEmailAddr = Yii::app()->request->getPost('email'); 230 231 $aFields = User::model()->findAllByAttributes(array('users_name' => $sUserName, 'email' => $sEmailAddr)); 232 233 // Preventing attacker from easily knowing whether the user and email address are valid or not (and slowing down brute force attacks) 234 usleep(rand(Yii::app()->getConfig("minforgottenpasswordemaildelay"), Yii::app()->getConfig("maxforgottenpasswordemaildelay"))); 235 $aData = []; 236 if (count($aFields) < 1 || ($aFields[0]['uid'] != 1 && !Permission::model()->hasGlobalPermission('auth_db', 'read', $aFields[0]['uid']))) { 237 // Wrong or unknown username and/or email. For security reasons, we don't show a fail message 238 $aData['message'] = '<br>'.gT('If the username and email address is valid and you are allowed to use the internal database authentication a new password has been sent to you.').'<br>'; 239 } else { 240 $aData['message'] = '<br>'.$this->_sendPasswordEmail($aFields[0]).'</br>'; 241 } 242 $this->_renderWrappedTemplate('authentication', 'message', $aData); 243 } 244 } 245 246 public static function runDbUpgrade() 247 { 248 // Check if the DB is up to date 249 if (Yii::app()->db->schema->getTable('{{surveys}}')) { 250 $sDBVersion = getGlobalSetting('DBVersion'); 251 if ((int) $sDBVersion < Yii::app()->getConfig('dbversionnumber')) { 252 // Try a silent update first 253 Yii::app()->loadHelper('update/updatedb'); 254 if (!db_upgrade_all(intval($sDBVersion), true)) { 255 Yii::app()->getController()->redirect(array('/admin/databaseupdate/sa/db')); 256 } 257 } 258 } 259 } 260 261 /** 262 * Send the forgot password email 263 * 264 * @param CActiveRecord User 265 */ 266 private function _sendPasswordEmail( $arUser) 267 { 268 $sFrom = Yii::app()->getConfig("siteadminname")." <".Yii::app()->getConfig("siteadminemail").">"; 269 $sTo = $arUser->email; 270 $sSubject = gT('User data'); 271 $sNewPass = createPassword(); 272 $sSiteName = Yii::app()->getConfig('sitename'); 273 $sSiteAdminBounce = Yii::app()->getConfig('siteadminbounce'); 274 275 $username = sprintf(gT('Username: %s'), $arUser['users_name']); 276 $password = sprintf(gT('New password: %s'), $sNewPass); 277 278 $body = array(); 279 $body[] = sprintf(gT('Your user data for accessing %s'), Yii::app()->getConfig('sitename')); 280 $body[] = $username; 281 $body[] = $password; 282 $body = implode("\n", $body); 283 284 if (SendEmailMessage($body, $sSubject, $sTo, $sFrom, $sSiteName, false, $sSiteAdminBounce)) { 285 User::updatePassword($arUser['uid'], $sNewPass); 286 // For security reasons, we don't show a successful message 287 $sMessage = gT('If the username and email address is valid and you are allowed to use the internal database authentication a new password has been sent to you.'); 288 } else { 289 $sMessage = gT('Email failed'); 290 } 291 292 return $sMessage; 293 } 294 295 /** 296 * Get's the summary 297 * @param string $sMethod login|logout 298 * @param string $sSummary Default summary 299 * @return string Summary 300 */ 301 private static function getSummary($sMethod = 'login', $sSummary = '') 302 { 303 if (!empty($sSummary)) { 304 return $sSummary; 305 } 306 307 switch ($sMethod) { 308 case 'logout' : 309 $sSummary = gT('Please log in first.'); 310 break; 311 312 case 'login' : 313 default : 314 $sSummary = '<br />'.sprintf(gT('Welcome %s!'), Yii::app()->session['full_name']).'<br /> '; 315 if (!empty(Yii::app()->session['redirect_after_login']) && strpos(Yii::app()->session['redirect_after_login'], 'logout') === false) { 316 Yii::app()->session['metaHeader'] = '<meta http-equiv="refresh"' 317 . ' content="1;URL='.Yii::app()->session['redirect_after_login'].'" />'; 318 $sSummary = '<p><font size="1"><i>'.gT('Reloading screen. Please wait.').'</i></font>'; 319 unset(Yii::app()->session['redirect_after_login']); 320 } 321 break; 322 } 323 324 return $sSummary; 325 } 326 327 /** 328 * Redirects a logged in user to the administration page 329 */ 330 private function _redirectIfLoggedIn() 331 { 332 if (!Yii::app()->user->getIsGuest()) { 333 $this->runDbUpgrade(); 334 Yii::app()->getController()->redirect(array('/admin')); 335 } 336 } 337 338 /** 339 * Redirect after login 340 * @return void 341 */ 342 private static function doRedirect() 343 { 344 self::runDbUpgrade(); 345 $returnUrl = App()->user->getReturnUrl(array('/admin')); 346 Yii::app()->getController()->redirect($returnUrl); 347 } 348 349 /** 350 * Renders template(s) wrapped in header and footer 351 * 352 * @param string $sAction Current action, the folder to fetch views from 353 * @param string $aViewUrls View url(s) 354 * @param array $aData Data to be passed on. Optional. 355 * @return void 356 */ 357 protected function _renderWrappedTemplate($sAction = 'authentication', $aViewUrls = array(), $aData = array(), $sRenderFile = false) 358 { 359 $aData['display']['menu_bars'] = false; 360 $aData['language'] = Yii::app()->getLanguage() != Yii::app()->getConfig("defaultlang") ? Yii::app()->getLanguage() : 'default'; 361 parent::_renderWrappedTemplate($sAction, $aViewUrls, $aData, $sRenderFile); 362 } 363 364} 365