1<?php
2/* Copyright (c) 1998-2013 ILIAS open source, Extended GPL, see docs/LICENSE */
3
4/**
5 * Password assistance facility for users who have forgotten their password
6 * or for users for whom no password has been assigned yet.
7 * @author  Werner Randelshofer <wrandels@hsw.fhz.ch>
8 * @author  Michael Jansen <mjansen@databay.de>
9 * @version $Id$
10 * @ingroup ServicesInit
11 */
12class ilPasswordAssistanceGUI
13{
14    const PERMANENT_LINK_TARGET_PW = 'pwassist';
15    const PERMANENT_LINK_TARGET_NAME = 'nameassist';
16
17    /**
18     * @var ilCtrl
19     */
20    protected $ctrl;
21
22    /**
23     * @var ilLanguage
24     */
25    protected $lng;
26
27    /**
28     * @var ilRbacReview
29     */
30    protected $rbacreview;
31
32    /**
33     * @var ilTemplate
34     */
35    protected $tpl;
36
37    /**
38     * @var ilSetting
39     */
40    protected $settings;
41
42    /**
43     * @var ILIAS
44     */
45    protected $ilias;
46
47    /**
48     * @var \ilErrorHandling
49     */
50    private $ilErr;
51
52
53    public function __construct()
54    {
55        global $DIC;
56
57        $this->ctrl = $DIC->ctrl();
58        $this->lng = $DIC->language();
59        $this->rbacreview = $DIC->rbac()->review();
60        $this->tpl = $DIC->ui()->mainTemplate();
61        $this->settings = $DIC->settings();
62        $this->ilias = $DIC['ilias'];
63        $this->ilErr = $DIC['ilErr'];
64    }
65
66    /**
67     * @return mixed
68     */
69    public function executeCommand()
70    {
71        // check hack attempts
72        if (!$this->settings->get('password_assistance')) {
73            $this->ilErr->raiseError($this->lng->txt('permission_denied'), $this->ilErr->FATAL);
74        }
75
76        // check correct setup
77        if (!$this->settings->get('setup_ok')) {
78            $this->ilErr->raiseError('Setup is not completed. Please run setup routine again.', $this->ilErr->FATAL);
79        }
80
81        // Change the language, if necessary.
82        // And load the 'pwassist' language module
83        $lang = $_GET['lang'];
84        if ($lang != null && $lang != '' && $this->lng->getLangKey() != $lang) {
85            $lng = new ilLanguage($lang);
86        }
87        $this->lng->loadLanguageModule('pwassist');
88
89        $cmd = $this->ctrl->getCmd();
90        $next_class = $this->ctrl->getNextClass($this);
91
92        switch ($next_class) {
93            default:
94                if ($cmd != '' && method_exists($this, $cmd)) {
95                    return $this->$cmd();
96                } else {
97                    if (!empty($_GET['key'])) {
98                        $this->showAssignPasswordForm();
99                    } else {
100                        $this->showAssistanceForm();
101                    }
102                }
103                break;
104        }
105    }
106
107    /**
108     * Returns the ILIAS http path without a trailing /
109     * @return string
110     */
111    protected function getBaseUrl() : string
112    {
113        return rtrim(ILIAS_HTTP_PATH, '/');
114    }
115
116    /**
117     * @param string $script
118     * @param array  $queryParameters
119     * @return string
120     */
121    protected function buildUrl(string $script, array $queryParameters) : string
122    {
123        $url = implode('/', [
124            $this->getBaseUrl(),
125            ltrim($script, '/')
126        ]);
127
128        $url = \ilUtil::appendUrlParameterString(
129            $url,
130            http_build_query($queryParameters, null, '&')
131        );
132
133        return $url;
134    }
135
136    /**
137     * @return ilPropertyFormGUI
138     */
139    protected function getAssistanceForm()
140    {
141        require_once 'Services/Form/classes/class.ilPropertyFormGUI.php';
142        $form = new ilPropertyFormGUI();
143
144        $form->setTitle($this->lng->txt('password_assistance'));
145        $form->setFormAction($this->ctrl->getFormAction($this, 'submitAssistanceForm'));
146        $form->setTarget('_parent');
147
148        $username = new ilTextInputGUI($this->lng->txt('username'), 'username');
149        $username->setRequired(true);
150        $form->addItem($username);
151
152        $email = new ilEMailInputGUI($this->lng->txt('email'), 'email');
153        $email->setRequired(true);
154        $form->addItem($email);
155
156        $form->addCommandButton('submitAssistanceForm', $this->lng->txt('submit'));
157
158        return $form;
159    }
160
161    /**
162     * @param ilPropertyFormGUI $form
163     */
164    public function showAssistanceForm(ilPropertyFormGUI $form = null)
165    {
166        ilStartUpGUI::initStartUpTemplate('tpl.pwassist_assistance.html', true);
167        $this->tpl->setVariable('IMG_PAGEHEADLINE', ilUtil::getImagePath('icon_auth.svg'));
168        $this->tpl->setVariable('TXT_PAGEHEADLINE', $this->lng->txt('password_assistance'));
169
170        $this->tpl->setVariable(
171            'TXT_ENTER_USERNAME_AND_EMAIL',
172            str_replace(
173                "\\n",
174                '<br />',
175                sprintf(
176                    $this->lng->txt('pwassist_enter_username_and_email'),
177                    '<a href="mailto:' . ilUtil::prepareFormOutput($this->settings->get('admin_email')) . '">' . ilUtil::prepareFormOutput($this->settings->get('admin_email')) . '</a>'
178                )
179            )
180        );
181
182        if (!$form) {
183            $form = $this->getAssistanceForm();
184        }
185        $this->tpl->setVariable('FORM', $form->getHTML());
186        $this->fillPermanentLink(self::PERMANENT_LINK_TARGET_PW);
187        $this->tpl->show();
188    }
189
190    /**
191     * Reads the submitted data from the password assistance form.
192     * The following form fields are read as HTTP POST parameters:
193     * username
194     * email
195     * If the submitted username and email address matches an entry in the user data
196     * table, then ILIAS creates a password assistance session for the user, and
197     * sends a password assistance mail to the email address.
198     * For details about the creation of the session and the e-mail see function
199     * sendPasswordAssistanceMail().
200     */
201    public function submitAssistanceForm()
202    {
203        $form = $this->getAssistanceForm();
204        if (!$form->checkInput()) {
205            $form->setValuesByPost();
206            $this->showAssistanceForm($form);
207            return;
208        }
209
210        $username = $form->getInput('username');
211        $email = $form->getInput('email');
212
213        $usrId = \ilObjUser::getUserIdByLogin($username);
214        if (!is_numeric($usrId) || !($usrId > 0)) {
215            \ilLoggerFactory::getLogger('usr')->info(sprintf(
216                'Could not process password assistance form (reason: no user found) %s / %s',
217                $username,
218                $email
219            ));
220
221            $this->showMessageForm(sprintf($this->lng->txt('pwassist_mail_sent'), $email));
222            return;
223        }
224
225        $defaultAuth = AUTH_LOCAL;
226        if ($GLOBALS['DIC']['ilSetting']->get('auth_mode')) {
227            $defaultAuth = $GLOBALS['DIC']['ilSetting']->get('auth_mode');
228        }
229
230        $user = new \ilObjUser($usrId);
231        $emailAddresses = array_map('strtolower', [$user->getEmail(), $user->getSecondEmail()]);
232
233        if (!in_array(strtolower($email), $emailAddresses)) {
234            if (0 === strlen(implode('', $emailAddresses))) {
235                \ilLoggerFactory::getLogger('usr')->info(sprintf(
236                    'Could not process password assistance form (reason: account without email addresses): %s / %s',
237                    $username,
238                    $email
239                ));
240            } else {
241                \ilLoggerFactory::getLogger('usr')->info(sprintf(
242                    'Could not process password assistance form (reason: account email addresses differ from input): %s / %s',
243                    $username,
244                    $email
245                ));
246            }
247        } elseif (
248            (
249                $user->getAuthMode(true) != AUTH_LOCAL ||
250                ($user->getAuthMode(true) == $defaultAuth && $defaultAuth != AUTH_LOCAL)
251            ) && !(
252                $user->getAuthMode(true) == AUTH_SAML
253            )
254        ) {
255            \ilLoggerFactory::getLogger('usr')->info(sprintf(
256                'Could not process password assistance form (reason: not permitted for accounts using external authentication sources): %s / %s',
257                $username,
258                $email
259            ));
260        } elseif (
261            $this->rbacreview->isAssigned($user->getId(), ANONYMOUS_ROLE_ID) ||
262            $this->rbacreview->isAssigned($user->getId(), SYSTEM_ROLE_ID)
263        ) {
264            \ilLoggerFactory::getLogger('usr')->info(sprintf(
265                'Could not process password assistance form (reason: not permitted for system user or anonymous): %s / %s',
266                $username,
267                $email
268            ));
269        } else {
270            $this->sendPasswordAssistanceMail($user);
271        }
272
273        $this->showMessageForm(sprintf($this->lng->txt('pwassist_mail_sent'), $email));
274    }
275
276    /**
277     * Creates (or reuses) a password assistance session, and sends a password
278     * assistance mail to the specified user.
279     * Note: To prevent DOS attacks, a new session is created only, if no session
280     * exists, or if the existing session has been expired.
281     * The password assistance mail contains an URL, which points to this script
282     * and contains the following URL parameters:
283     * client_id
284     * key
285     * @param $userObj ilObjUser
286     */
287    public function sendPasswordAssistanceMail(ilObjUser $userObj)
288    {
289        global $DIC;
290
291        require_once 'include/inc.pwassist_session_handler.php';
292
293        // Check if we need to create a new session
294        $pwassist_session = db_pwassist_session_find($userObj->getId());
295        if (
296            !is_array($pwassist_session) ||
297            count($pwassist_session) == 0 ||
298            $pwassist_session['expires'] < time() ||
299            true // comment by mjansen: wtf? :-)
300        ) {
301            // Create a new session id
302            // #9700 - this didn't do anything before?!
303            // db_set_save_handler();
304            session_start();
305            $pwassist_session['pwassist_id'] = db_pwassist_create_id();
306            session_destroy();
307            db_pwassist_session_write(
308                $pwassist_session['pwassist_id'],
309                3600,
310                $userObj->getId()
311            );
312        }
313
314        $pwassist_url = $this->buildUrl(
315            'pwassist.php',
316            [
317                'client_id' => $this->ilias->getClientId(),
318                'lang' => $this->lng->getLangKey(),
319                'key' => $pwassist_session['pwassist_id']
320            ]
321        );
322
323        $alternative_pwassist_url = $this->buildUrl(
324            'pwassist.php',
325            [
326                'client_id' => $this->ilias->getClientId(),
327                'lang' => $this->lng->getLangKey(),
328                'key' => $pwassist_session['pwassist_id']
329            ]
330        );
331
332        /** @var ilMailMimeSenderFactory $senderFactory */
333        $senderFactory = $DIC["mail.mime.sender.factory"];
334        $sender = $senderFactory->system();
335
336        $mm = new ilMimeMail();
337        $mm->Subject($this->lng->txt('pwassist_mail_subject'), true);
338        $mm->From($sender);
339        $mm->To($userObj->getEmail());
340        $mm->Body(
341            str_replace(
342                array("\\n", "\\t"),
343                array("\n", "\t"),
344                sprintf(
345                    $this->lng->txt('pwassist_mail_body'),
346                    $pwassist_url,
347                    $this->getBaseUrl() . '/',
348                    $_SERVER['REMOTE_ADDR'],
349                    $userObj->getLogin(),
350                    'mailto:' . $DIC->settings()->get("admin_email"),
351                    $alternative_pwassist_url
352                )
353            )
354        );
355        $mm->Send();
356    }
357
358    /**
359     * @param string $pwassist_id
360     * @return ilPropertyFormGUI
361     */
362    protected function getAssignPasswordForm($pwassist_id)
363    {
364        require_once 'Services/Form/classes/class.ilPropertyFormGUI.php';
365        $form = new ilPropertyFormGUI();
366
367        $form->setFormAction($this->ctrl->getFormAction($this, 'submitAssignPasswordForm'));
368        $form->setTarget('_parent');
369
370        $username = new ilTextInputGUI($this->lng->txt('username'), 'username');
371        $username->setRequired(true);
372        $form->addItem($username);
373
374        $password = new ilPasswordInputGUI($this->lng->txt('password'), 'password');
375        $password->setInfo(\ilUtil::getPasswordRequirementsInfo());
376        $password->setRequired(true);
377        $form->addItem($password);
378
379        $key = new ilHiddenInputGUI('key');
380        $key->setValue($pwassist_id);
381        $form->addItem($key);
382
383        $form->addCommandButton('submitAssignPasswordForm', $this->lng->txt('submit'));
384
385        return $form;
386    }
387
388    /**
389     * Assign password form.
390     * This form is used to assign a password to a username.
391     * To use this form, the following data must be provided as HTTP GET parameter,
392     * or in argument pwassist_id:
393     * key
394     * The key is used to retrieve the password assistance session.
395     * If the key is missing, or if the password assistance session has expired, the
396     * password assistance form will be shown instead of this form.
397     * @param ilPropertyFormGUI $form
398     * @param string            $pwassist_id
399     */
400    public function showAssignPasswordForm(ilPropertyFormGUI $form = null, $pwassist_id = '')
401    {
402        require_once 'include/inc.pwassist_session_handler.php';
403        require_once 'Services/Language/classes/class.ilLanguage.php';
404
405        // Retrieve form data
406        if (!$pwassist_id) {
407            $pwassist_id = $_GET['key'];
408        }
409
410        // Retrieve the session, and check if it is valid
411        $pwassist_session = db_pwassist_session_read($pwassist_id);
412        if (
413            !is_array($pwassist_session) ||
414            count($pwassist_session) == 0 ||
415            $pwassist_session['expires'] < time()
416        ) {
417            ilUtil::sendFailure($this->lng->txt('pwassist_session_expired'));
418            $this->showAssistanceForm(null);
419        } else {
420            ilStartUpGUI::initStartUpTemplate('tpl.pwassist_assignpassword.html', true);
421            $this->tpl->setVariable('IMG_PAGEHEADLINE', ilUtil::getImagePath('icon_auth.svg'));
422            $this->tpl->setVariable('TXT_PAGEHEADLINE', $this->lng->txt('password_assistance'));
423
424            $this->tpl->setVariable('TXT_ENTER_USERNAME_AND_NEW_PASSWORD', $this->lng->txt('pwassist_enter_username_and_new_password'));
425
426            if (!$form) {
427                $form = $this->getAssignPasswordForm($pwassist_id);
428            }
429            $this->tpl->setVariable('FORM', $form->getHTML());
430            $this->fillPermanentLink(self::PERMANENT_LINK_TARGET_PW);
431            $this->tpl->show();
432        }
433    }
434
435    /**
436     * Reads the submitted data from the password assistance form.
437     * The following form fields are read as HTTP POST parameters:
438     * key
439     * username
440     * password1
441     * password2
442     * The key is used to retrieve the password assistance session.
443     * If the key is missing, or if the password assistance session has expired, the
444     * password assistance form will be shown instead of this form.
445     * If the password assistance session is valid, and if the username matches the
446     * username, for which the password assistance has been requested, and if the
447     * new password is valid, ILIAS assigns the password to the user.
448     * Note: To prevent replay attacks, the session is deleted when the
449     * password has been assigned successfully.
450     */
451    public function submitAssignPasswordForm()
452    {
453        require_once 'include/inc.pwassist_session_handler.php';
454
455        // We need to fetch this before form instantiation
456        $pwassist_id = ilUtil::stripSlashes($_POST['key']);
457
458        $form = $this->getAssignPasswordForm($pwassist_id);
459        if (!$form->checkInput()) {
460            $form->setValuesByPost();
461            $this->showAssistanceForm($form);
462            return;
463        }
464
465        $username = $form->getInput('username');
466        $password = $form->getInput('password');
467        $pwassist_id = $form->getInput('key');
468
469        // Retrieve the session
470        $pwassist_session = db_pwassist_session_read($pwassist_id);
471
472        if (
473            !is_array($pwassist_session) ||
474            count($pwassist_session) == 0 ||
475            $pwassist_session['expires'] < time()
476        ) {
477            ilUtil::sendFailure(str_replace("\\n", '', $this->lng->txt('pwassist_session_expired')));
478            $form->setValuesByPost();
479            $this->showAssistanceForm($form);
480            return;
481        } else {
482            $is_successful = true;
483            $message = '';
484
485            $userObj = \ilObjectFactory::getInstanceByObjId($pwassist_session['user_id'], false);
486            if (!$userObj || !($userObj instanceof \ilObjUser)) {
487                $message = $this->lng->txt('user_does_not_exist');
488                $is_successful = false;
489            }
490
491            // check if the username entered by the user matches the
492            // one of the user object.
493            if ($is_successful && strcasecmp($userObj->getLogin(), $username) != 0) {
494                $message = $this->lng->txt('pwassist_login_not_match');
495                $is_successful = false;
496            }
497
498            $error_lng_var = '';
499            if (!ilUtil::isPasswordValidForUserContext($password, $userObj, $error_lng_var)) {
500                $message = $this->lng->txt($error_lng_var);
501                $is_successful = false;
502            }
503
504            // End of validation
505            // If the validation was successful, we change the password of the
506            // user.
507            // ------------------
508            if ($is_successful) {
509                $is_successful = $userObj->resetPassword($password, $password);
510                if (!$is_successful) {
511                    $message = $this->lng->txt('passwd_invalid');
512                }
513            }
514
515            // If we are successful so far, we update the user object.
516            // ------------------
517            if ($is_successful) {
518                $userObj->update();
519            }
520
521            // If we are successful, we destroy the password assistance
522            // session and redirect to the login page.
523            // Else we display the form again along with an error message.
524            // ------------------
525            if ($is_successful) {
526                db_pwassist_session_destroy($pwassist_id);
527                $this->showMessageForm(sprintf($this->lng->txt('pwassist_password_assigned'), $username));
528            } else {
529                ilUtil::sendFailure(str_replace("\\n", '', $message));
530                $form->setValuesByPost();
531                $this->showAssignPasswordForm($form, $pwassist_id);
532            }
533        }
534    }
535
536    /**
537     * @return ilPropertyFormGUI
538     */
539    protected function getUsernameAssistanceForm()
540    {
541        require_once 'Services/Form/classes/class.ilPropertyFormGUI.php';
542        $form = new ilPropertyFormGUI();
543
544        $form->setFormAction($this->ctrl->getFormAction($this, 'submitUsernameAssistanceForm'));
545        $form->setTarget('_parent');
546
547        $email = new ilTextInputGUI($this->lng->txt('email'), 'email');
548        $email->setRequired(true);
549        $form->addItem($email);
550
551        $form->addCommandButton('submitUsernameAssistanceForm', $this->lng->txt('submit'));
552
553        return $form;
554    }
555
556    /**
557     * Shows the password assistance form.
558     * This form is used to request a password assistance mail from ILIAS.
559     * This form contains the following fields:
560     * username
561     * email
562     * When the user submits the form, then this script is invoked with the cmd
563     * 'submitAssistanceForm'.
564     * @param ilPropertyFormGUI $form
565     */
566    public function showUsernameAssistanceForm(ilPropertyFormGUI $form = null)
567    {
568        ilStartUpGUI::initStartUpTemplate('tpl.pwassist_username_assistance.html', true);
569        $this->tpl->setVariable('IMG_PAGEHEADLINE', ilUtil::getImagePath('icon_auth.svg'));
570        $this->tpl->setVariable('TXT_PAGEHEADLINE', $this->lng->txt('password_assistance'));
571
572        $this->tpl->setVariable(
573            'TXT_ENTER_USERNAME_AND_EMAIL',
574            str_replace(
575                "\\n",
576                '<br />',
577                sprintf(
578                    $this->lng->txt('pwassist_enter_email'),
579                    '<a href="mailto:' . ilUtil::prepareFormOutput($this->settings->get('admin_email')) . '">' . ilUtil::prepareFormOutput($this->settings->get('admin_email')) . '</a>'
580                )
581            )
582        );
583
584        if (!$form) {
585            $form = $this->getUsernameAssistanceForm();
586        }
587        $this->tpl->setVariable('FORM', $form->getHTML());
588        $this->fillPermanentLink(self::PERMANENT_LINK_TARGET_NAME);
589        $this->tpl->show();
590    }
591
592    /**
593     * Reads the submitted data from the password assistance form.
594     * The following form fields are read as HTTP POST parameters:
595     * username
596     * email
597     * If the submitted username and email address matches an entry in the user data
598     * table, then ILIAS creates a password assistance session for the user, and
599     * sends a password assistance mail to the email address.
600     * For details about the creation of the session and the e-mail see function
601     * sendPasswordAssistanceMail().
602     */
603    public function submitUsernameAssistanceForm()
604    {
605        require_once 'Services/User/classes/class.ilObjUser.php';
606        require_once 'Services/Utilities/classes/class.ilUtil.php';
607
608        $form = $this->getUsernameAssistanceForm();
609        if (!$form->checkInput()) {
610            $form->setValuesByPost();
611            $this->showUsernameAssistanceForm($form);
612
613            return;
614        }
615
616        $email = $form->getInput('email');
617        $logins = ilObjUser::getUserLoginsByEmail($email);
618
619        if (is_array($logins) && count($logins) > 0) {
620            $this->sendUsernameAssistanceMail($email, $logins);
621        } else {
622            \ilLoggerFactory::getLogger('usr')->info(sprintf(
623                'Could not sent username assistance emails to (reason: no user found): %s',
624                $email
625            ));
626        }
627
628        $this->showMessageForm($this->lng->txt('pwassist_mail_sent_generic'));
629    }
630
631    /**
632     * Creates (or reuses) a password assistance session, and sends a password
633     * assistance mail to the specified user.
634     * Note: To prevent DOS attacks, a new session is created only, if no session
635     * exists, or if the existing session has been expired.
636     * The password assistance mail contains an URL, which points to this script
637     * and contains the following URL parameters:
638     * client_id
639     * key
640     * @param $email
641     * @param $logins
642     */
643    public function sendUsernameAssistanceMail($email, array $logins)
644    {
645        global $DIC;
646
647        require_once 'Services/Mail/classes/class.ilMailbox.php';
648        require_once 'Services/Mail/classes/class.ilMail.php';
649        require_once 'Services/Mail/classes/class.ilMimeMail.php';
650        require_once 'include/inc.pwassist_session_handler.php';
651
652        $login_url = $this->buildUrl(
653            'pwassist.php',
654            [
655                'client_id' => $this->ilias->getClientId(),
656                'lang' => $this->lng->getLangKey()
657            ]
658        );
659
660        /** @var ilMailMimeSenderFactory $senderFactory */
661        $senderFactory = $DIC["mail.mime.sender.factory"];
662        $sender = $senderFactory->system();
663
664        $mm = new ilMimeMail();
665        $mm->Subject($this->lng->txt('pwassist_mail_subject'), true);
666        $mm->From($sender);
667        $mm->To($email);
668        $mm->Body(
669            str_replace(
670                array("\\n", "\\t"),
671                array("\n", "\t"),
672                sprintf(
673                    $this->lng->txt('pwassist_username_mail_body'),
674                    join(",\n", $logins),
675                    $this->getBaseUrl() . '/',
676                    $_SERVER['REMOTE_ADDR'],
677                    $email,
678                    'mailto:' . $DIC->settings()->get("admin_email"),
679                    $login_url
680                )
681            )
682        );
683        $mm->Send();
684    }
685
686    /**
687     * This form is used to show a message to the user.
688     * @param string $text
689     */
690    public function showMessageForm($text)
691    {
692        ilStartUpGUI::initStartUpTemplate('tpl.pwassist_message.html', true);
693        $this->tpl->setVariable('TXT_PAGEHEADLINE', $this->lng->txt('password_assistance'));
694        $this->tpl->setVariable('IMG_PAGEHEADLINE', ilUtil::getImagePath('icon_auth.svg'));
695
696        $this->tpl->setVariable('TXT_TEXT', str_replace("\\n", '<br />', $text));
697        $this->fillPermanentLink(self::PERMANENT_LINK_TARGET_NAME);
698        $this->tpl->show();
699    }
700
701    /**
702     * @param string $context
703     */
704    protected function fillPermanentLink($context)
705    {
706        $this->tpl->setPermanentLink('usr', null, $context);
707    }
708}
709