1<?php
2/**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9namespace Piwik\Plugins\Login;
10
11use Exception;
12use Piwik\Access;
13use Piwik\Auth\Password;
14use Piwik\Common;
15use Piwik\Config;
16use Piwik\Container\StaticContainer;
17use Piwik\Date;
18use Piwik\Log;
19use Piwik\Nonce;
20use Piwik\Piwik;
21use Piwik\Plugins\Login\Security\BruteForceDetection;
22use Piwik\Plugins\UsersManager\Model AS UsersModel;
23use Piwik\Plugins\UsersManager\UserUpdater;
24use Piwik\QuickForm2;
25use Piwik\Session;
26use Piwik\Url;
27use Piwik\UrlHelper;
28use Piwik\View;
29
30/**
31 * Login controller
32 * @api
33 */
34class Controller extends \Piwik\Plugin\ControllerAdmin
35{
36    const NONCE_CONFIRMRESETPASSWORD = 'loginConfirmResetPassword';
37
38    /**
39     * @var PasswordResetter
40     */
41    protected $passwordResetter;
42
43    /**
44     * @var Auth
45     */
46    protected $auth;
47
48    /**
49     * @var \Piwik\Session\SessionInitializer
50     */
51    protected $sessionInitializer;
52
53    /**
54     * @var BruteForceDetection
55     */
56    protected $bruteForceDetection;
57
58    /**
59     * @var SystemSettings
60     */
61    protected $systemSettings;
62
63    /*
64     * @var PasswordVerifier
65     */
66    protected $passwordVerify;
67
68    /**
69     * Constructor.
70     *
71     * @param PasswordResetter $passwordResetter
72     * @param AuthInterface $auth
73     * @param SessionInitializer $sessionInitializer
74     * @param PasswordVerifier $passwordVerify
75     * @param BruteForceDetection $bruteForceDetection
76     * @param SystemSettings $systemSettings
77     */
78    public function __construct($passwordResetter = null, $auth = null, $sessionInitializer = null, $passwordVerify = null, $bruteForceDetection = null, $systemSettings = null)
79    {
80        parent::__construct();
81
82        if (empty($passwordResetter)) {
83            $passwordResetter = new PasswordResetter();
84        }
85        $this->passwordResetter = $passwordResetter;
86
87        if (empty($auth)) {
88            $auth = StaticContainer::get('Piwik\Auth');
89        }
90        $this->auth = $auth;
91
92        if (empty($passwordVerify)) {
93            $passwordVerify = StaticContainer::get('Piwik\Plugins\Login\PasswordVerifier');
94        }
95        $this->passwordVerify = $passwordVerify;
96
97        if (empty($sessionInitializer)) {
98            $sessionInitializer = new \Piwik\Session\SessionInitializer();
99        }
100        $this->sessionInitializer = $sessionInitializer;
101
102        if (empty($bruteForceDetection)) {
103            $bruteForceDetection = StaticContainer::get('Piwik\Plugins\Login\Security\BruteForceDetection');
104        }
105        $this->bruteForceDetection = $bruteForceDetection;
106
107        if (empty($systemSettings)) {
108            $systemSettings = StaticContainer::get('Piwik\Plugins\Login\SystemSettings');
109        }
110        $this->systemSettings = $systemSettings;
111    }
112
113    /**
114     * Default action
115     *
116     * @return string
117     */
118    function index()
119    {
120        return $this->login();
121    }
122
123    /**
124     * Login form
125     *
126     * @param string $messageNoAccess Access error message
127     * @param bool $infoMessage
128     * @internal param string $currentUrl Current URL
129     * @return string
130     */
131    function login($messageNoAccess = null, $infoMessage = false)
132    {
133        $form = new FormLogin();
134        if ($form->validate()) {
135            $nonce = $form->getSubmitValue('form_nonce');
136            $messageNoAccess = Nonce::verifyNonceWithErrorMessage('Login.login', $nonce, null);
137
138            // validate if there is error message
139            if ($messageNoAccess === "") {
140                $loginOrEmail = $form->getSubmitValue('form_login');
141                $login = $this->getLoginFromLoginOrEmail($loginOrEmail);
142
143                $password = $form->getSubmitValue('form_password');
144                try {
145                    $this->authenticateAndRedirect($login, $password);
146                } catch (Exception $e) {
147                    $messageNoAccess = $e->getMessage();
148                }
149            }
150        }
151
152        if ($messageNoAccess) {
153            http_response_code(403);
154        }
155
156        $view = new View('@Login/login');
157        $view->AccessErrorString = $messageNoAccess;
158        $view->infoMessage = nl2br($infoMessage);
159        $view->addForm($form);
160        $this->configureView($view);
161        self::setHostValidationVariablesView($view);
162
163        return $view->render();
164    }
165
166    private function getLoginFromLoginOrEmail($loginOrEmail)
167    {
168        $model = new UsersModel();
169        if (!$model->userExists($loginOrEmail)) {
170            $user = $model->getUserByEmail($loginOrEmail);
171            if (!empty($user)) {
172                return $user['login'];
173            }
174        }
175
176        return $loginOrEmail;
177    }
178
179    /**
180     * Configure common view properties
181     *
182     * @param View $view
183     */
184    protected function configureView($view)
185    {
186        $this->setBasicVariablesNoneAdminView($view);
187
188        $view->linkTitle = Piwik::getRandomTitle();
189
190        // crsf token: don't trust the submitted value; generate/fetch it from session data
191        $view->nonce = Nonce::getNonce('Login.login');
192    }
193
194    public function confirmPassword()
195    {
196        Piwik::checkUserIsNotAnonymous();
197        Piwik::checkUserHasSomeViewAccess();
198
199        if (!$this->passwordVerify->hasPasswordVerifyBeenRequested()) {
200            throw new Exception('Not available');
201        }
202
203        if (!Url::isValidHost()) {
204            throw new Exception("Cannot confirm password with untrusted hostname!");
205        }
206
207        $nonceKey = 'confirmPassword';
208        $messageNoAccess = '';
209
210        if (!empty($_POST)) {
211            $nonce = Common::getRequestVar('nonce', null, 'string', $_POST);
212            $password = Common::getRequestVar('password', null, 'string', $_POST);
213            if ($password) {
214                $password = Common::unsanitizeInputValue($password);
215            }
216            $errorMessage = Nonce::verifyNonceWithErrorMessage($nonceKey, $nonce);
217            if ($errorMessage !== "") {
218                $messageNoAccess = $errorMessage;
219            } elseif ($this->passwordVerify->isPasswordCorrect(Piwik::getCurrentUserLogin(), $password)) {
220                $this->passwordVerify->setPasswordVerifiedCorrectly();
221                return;
222            } else {
223                $messageNoAccess = Piwik::translate('Login_WrongPasswordEntered');
224            }
225        }
226
227        return $this->renderTemplate('@Login/confirmPassword', array(
228            'nonce' => Nonce::getNonce($nonceKey),
229            'AccessErrorString' => $messageNoAccess,
230            'loginPlugin' => Piwik::getLoginPluginName(),
231        ));
232    }
233
234    /**
235     * Form-less login
236     * @see how to use it on http://piwik.org/faq/how-to/#faq_30
237     * @throws Exception
238     * @return void
239     */
240    function logme()
241    {
242        if (Config::getInstance()->General['login_allow_logme'] == 0) {
243            throw new Exception('This functionality has been disabled in config');
244        }
245
246        $password = Common::getRequestVar('password', null, 'string');
247
248        $login = Common::getRequestVar('login', null, 'string');
249        if (Piwik::hasTheUserSuperUserAccess($login)) {
250            throw new Exception(Piwik::translate('Login_ExceptionInvalidSuperUserAccessAuthenticationMethod', array("logme")));
251        }
252
253        $currentUrl = 'index.php';
254
255        if ($this->idSite) {
256            $currentUrl .= '?idSite=' . $this->idSite;
257        }
258
259        $urlToRedirect = Common::getRequestVar('url', $currentUrl, 'string');
260        $urlToRedirect = Common::unsanitizeInputValue($urlToRedirect);
261
262        $this->authenticateAndRedirect($login, $password, $urlToRedirect, $passwordHashed = true);
263    }
264
265    public function bruteForceLog()
266    {
267        Piwik::checkUserHasSuperUserAccess();
268
269        return $this->renderTemplate('bruteForceLog', array(
270            'blockedIps' => $this->bruteForceDetection->getCurrentlyBlockedIps(),
271            'blacklistedIps' => $this->systemSettings->blacklistedBruteForceIps->getValue()
272        ));
273    }
274
275    /**
276     * Error message shown when an AJAX request has no access
277     *
278     * @param string $errorMessage
279     * @return string
280     */
281    public function ajaxNoAccess($errorMessage)
282    {
283        return sprintf(
284            '<div class="alert alert-danger">
285                <p><strong>%s:</strong> %s</p>
286                <p><a href="%s">%s</a></p>
287            </div>',
288            Piwik::translate('General_Error'),
289            htmlentities($errorMessage, Common::HTML_ENCODING_QUOTE_STYLE, 'UTF-8', $doubleEncode = false),
290            'index.php?module=' . Piwik::getLoginPluginName(),
291            Piwik::translate('Login_LogIn')
292        );
293    }
294
295    /**
296     * Authenticate user and password.  Redirect if successful.
297     *
298     * @param string $login user name
299     * @param string $password plain-text or hashed password
300     * @param string $urlToRedirect URL to redirect to, if successfully authenticated
301     * @param bool $passwordHashed indicates if $password is hashed
302     * @return string failure message if unable to authenticate
303     */
304    protected function authenticateAndRedirect($login, $password, $urlToRedirect = false, $passwordHashed = false)
305    {
306        Nonce::discardNonce('Login.login');
307
308        $this->auth->setLogin($login);
309        if ($passwordHashed === false) {
310            $this->auth->setPassword($password);
311        } else {
312            $this->auth->setPasswordHash($password);
313        }
314
315        $this->sessionInitializer->initSession($this->auth);
316
317        // remove password reset entry if it exists
318        $this->passwordResetter->removePasswordResetInfo($login);
319
320        $parsedUrl = parse_url($urlToRedirect);
321
322        // only use redirect url if host is trusted
323        if (!empty($parsedUrl['host']) && !Url::isValidHost($parsedUrl['host'])) {
324            $e = new \Piwik\Exception\Exception('The redirect URL host is not valid, it is not a trusted host. If this URL is trusted, you can allow this in your config.ini.php file by adding the line <i>trusted_hosts[] = "'.Common::sanitizeInputValue($parsedUrl['host']).'"</i> under <i>[General]</i>');
325            $e->setIsHtmlMessage();
326            throw $e;
327        }
328
329        if (empty($urlToRedirect)) {
330            $redirect = Common::unsanitizeInputValue(Common::getRequestVar('form_redirect', false));
331            $redirectParams = UrlHelper::getArrayFromQueryString(UrlHelper::getQueryFromUrl($redirect));
332            $module = Common::getRequestVar('module', '', 'string', $redirectParams);
333            // when module is login, we redirect to home...
334            if (!empty($module) && $module !== 'Login' && $module !== Piwik::getLoginPluginName() && $redirect) {
335                $host = Url::getHostFromUrl($redirect);
336                $currentHost = Url::getHost();
337                $currentHost = explode(':', $currentHost, 2)[0];
338
339                // we only redirect to a trusted host
340                if (!empty($host) && !empty($currentHost) && $host == $currentHost && Url::isValidHost($host)
341                ) {
342                    $urlToRedirect = $redirect;
343                }
344            }
345        }
346
347        if (empty($urlToRedirect)) {
348            $urlToRedirect = Url::getCurrentUrlWithoutQueryString();
349        }
350
351        Url::redirectToUrl($urlToRedirect);
352    }
353
354    /**
355     * Reset password action. Stores new password as hash and sends email
356     * to confirm use.
357     *
358     */
359    function resetPassword()
360    {
361        $infoMessage = null;
362        $formErrors = null;
363
364        $form = new FormResetPassword();
365        if ($form->validate()) {
366            $nonce = $form->getSubmitValue('form_nonce');
367            $errorMessage = Nonce::verifyNonceWithErrorMessage('Login.login', $nonce);
368            if ($errorMessage === "") {
369                $formErrors = $this->resetPasswordFirstStep($form);
370                if (empty($formErrors)) {
371                    $infoMessage = Piwik::translate('Login_ConfirmationLinkSent');
372                }
373            } else {
374                $formErrors = array($errorMessage);
375            }
376        } else {
377            // if invalid, display error
378            $formData = $form->getFormData();
379            $formErrors = $formData['errors'];
380        }
381
382        $view = new View('@Login/resetPassword');
383        $view->infoMessage = $infoMessage;
384        $view->formErrors = $formErrors;
385
386        return $view->render();
387    }
388
389    /**
390     * Saves password reset info and sends confirmation email.
391     *
392     * @param QuickForm2 $form
393     * @return array Error message(s) if an error occurs.
394     */
395    protected function resetPasswordFirstStep($form)
396    {
397        $loginMail = $form->getSubmitValue('form_login');
398        $password  = $form->getSubmitValue('form_password');
399
400        try {
401            $this->passwordResetter->initiatePasswordResetProcess($loginMail, $password);
402        } catch (Exception $ex) {
403            Log::debug($ex);
404
405            return array($ex->getMessage());
406        }
407
408        return null;
409    }
410
411    /**
412     * Password reset confirmation action. Finishes the password reset process.
413     * Users visit this action from a link supplied in an email.
414     */
415    public function confirmResetPassword()
416    {
417        if (!Url::isValidHost()) {
418            throw new Exception("Cannot confirm reset password with untrusted hostname!");
419        }
420
421        $errorMessage = null;
422        $passwordHash = null;
423
424        $login = Common::getRequestVar('login');
425        $resetToken = Common::getRequestVar('resetToken');
426
427        try {
428            $passwordHash = $this->passwordResetter->checkValidConfirmPasswordToken($login, $resetToken);
429        } catch (Exception $ex) {
430            Log::debug($ex);
431
432            $errorMessage = $ex->getMessage();
433        }
434
435        if (!empty($errorMessage)) {
436            return $this->login($errorMessage);
437        }
438
439        if (!empty($_POST['nonce'])
440            && !empty($_POST['mtmpasswordconfirm'])
441            && !empty($resetToken)
442            && !empty($login)
443            && !empty($passwordHash)
444            && empty($errorMessage)) {
445            Nonce::checkNonce(self::NONCE_CONFIRMRESETPASSWORD, $_POST['nonce']);
446            if ($this->passwordResetter->doesResetPasswordHashMatchesPassword($_POST['mtmpasswordconfirm'], $passwordHash)) {
447                $this->passwordResetter->setHashedPasswordForLogin($login, $passwordHash);
448                return $this->resetPasswordSuccess();
449            } else {
450                $errorMessage = Piwik::translate('Login_ConfirmPasswordResetWrongPassword');
451            }
452        }
453
454        $nonce = Nonce::getNonce(self::NONCE_CONFIRMRESETPASSWORD);
455
456        return $this->renderTemplateAs('confirmResetPassword', array(
457            'nonce' => $nonce,
458            'errorMessage' => $errorMessage
459        ), 'basic');
460    }
461
462    /**
463     * The action used after a password is successfully reset. Displays the login
464     * screen with an extra message. A separate action is used instead of returning
465     * the HTML in confirmResetPassword so the resetToken won't be in the URL.
466     */
467    public function resetPasswordSuccess()
468    {
469        $_POST = array(); // prevent showing error message username and password is missing
470        return $this->login($errorMessage = null, $infoMessage = Piwik::translate('Login_PasswordChanged'));
471    }
472
473    /**
474     * Clear session information
475     *
476     * @return void
477     */
478    public static function clearSession()
479    {
480        $sessionFingerprint = new Session\SessionFingerprint();
481        $sessionFingerprint->clear();
482
483        Session::expireSessionCookie();
484    }
485
486    /**
487     * Logout current user
488     *
489     * @return void
490     */
491    public function logout()
492    {
493        Piwik::postEvent('Login.logout', array(Piwik::getCurrentUserLogin()));
494
495        self::clearSession();
496
497        $logoutUrl = @Config::getInstance()->General['login_logout_url'];
498        if (empty($logoutUrl)) {
499            Piwik::redirectToModule('CoreHome');
500        } else {
501            Url::redirectToUrl($logoutUrl);
502        }
503    }
504}
505