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 />&nbsp;';
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