001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ui.login;
029
030import org.opencms.ade.configuration.CmsADEManager;
031import org.opencms.db.CmsLoginMessage;
032import org.opencms.db.CmsUserSettings;
033import org.opencms.file.CmsObject;
034import org.opencms.file.CmsProject;
035import org.opencms.file.CmsRequestContext;
036import org.opencms.file.CmsUser;
037import org.opencms.i18n.CmsMessageContainer;
038import org.opencms.i18n.CmsResourceBundleLoader;
039import org.opencms.jsp.CmsJspActionElement;
040import org.opencms.jsp.CmsJspLoginBean;
041import org.opencms.main.CmsBroadcast.ContentMode;
042import org.opencms.main.CmsException;
043import org.opencms.main.CmsLog;
044import org.opencms.main.OpenCms;
045import org.opencms.security.CmsCustomLoginException;
046import org.opencms.security.CmsRole;
047import org.opencms.security.CmsUserLog;
048import org.opencms.security.twofactor.CmsSecondFactorInfo;
049import org.opencms.security.twofactor.CmsTwoFactorAuthenticationHandler;
050import org.opencms.ui.A_CmsDialogContext;
051import org.opencms.ui.A_CmsUI;
052import org.opencms.ui.CmsVaadinUtils;
053import org.opencms.ui.I_CmsDialogContext;
054import org.opencms.ui.I_CmsDialogContext.ContextType;
055import org.opencms.ui.Messages;
056import org.opencms.ui.apps.CmsAppHierarchyConfiguration;
057import org.opencms.ui.apps.CmsAppWorkplaceUi;
058import org.opencms.ui.apps.CmsFileExplorerConfiguration;
059import org.opencms.ui.apps.CmsSitemapEditorConfiguration;
060import org.opencms.ui.components.CmsBasicDialog;
061import org.opencms.ui.components.CmsBasicDialog.DialogWidth;
062import org.opencms.ui.dialogs.CmsUserDataDialog;
063import org.opencms.ui.login.CmsLoginHelper.LoginParameters;
064import org.opencms.util.CmsStringUtil;
065import org.opencms.util.CmsUUID;
066import org.opencms.workplace.CmsLoginUserAgreement;
067import org.opencms.workplace.CmsWorkplace;
068import org.opencms.workplace.CmsWorkplaceLoginHandler;
069import org.opencms.workplace.CmsWorkplaceManager;
070import org.opencms.workplace.CmsWorkplaceSettings;
071
072import java.io.IOException;
073import java.text.SimpleDateFormat;
074import java.util.Collection;
075import java.util.Date;
076import java.util.List;
077import java.util.MissingResourceException;
078import java.util.ResourceBundle;
079
080import javax.servlet.http.HttpServletRequest;
081import javax.servlet.http.HttpServletResponse;
082import javax.servlet.http.HttpSession;
083
084import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
085import org.apache.commons.lang3.builder.ToStringStyle;
086import org.apache.commons.logging.Log;
087
088import com.vaadin.server.Page;
089import com.vaadin.server.VaadinService;
090import com.vaadin.server.VaadinServletRequest;
091import com.vaadin.server.VaadinServletResponse;
092import com.vaadin.ui.Component;
093import com.vaadin.ui.UI;
094import com.vaadin.ui.Window;
095
096/**
097 * Controller class which actually handles the login dialog logic.<p>
098 */
099public class CmsLoginController {
100
101    /**
102     * Represents the login target information.<p>
103     */
104    public static class CmsLoginTargetInfo {
105
106        /** The password. */
107        private String m_password;
108
109        /** The login target. */
110        private String m_target;
111
112        /** The user. */
113        private String m_user;
114
115        /**
116         * Creates a new instance.<p>
117         *
118         * @param target the login target
119         * @param user the user name
120         * @param password the password
121         */
122        public CmsLoginTargetInfo(String target, String user, String password) {
123
124            super();
125            m_target = target;
126            m_user = user;
127            m_password = password;
128        }
129
130        /**
131         * Returns the password.<p>
132         *
133         * @return the password
134         */
135        public String getPassword() {
136
137            return m_password;
138        }
139
140        /**
141         * Returns the target.<p>
142         *
143         * @return the target
144         */
145        public String getTarget() {
146
147            return m_target;
148        }
149
150        /**
151         * Returns the user.<p>
152         *
153         * @return the user
154         */
155        public String getUser() {
156
157            return m_user;
158        }
159    }
160
161    /**
162     * The login context.
163     */
164    public static class LoginContext {
165
166        /** The CMS context. */
167        private CmsObject m_cms;
168
169        /** The second factor information. */
170        private CmsSecondFactorInfo m_secondFactorInfo;
171
172        /** The user being logged in. */
173        private CmsUser m_user;
174
175        /**
176         * Gets the CmsObject.
177         *
178         * @return the CmsObject
179         */
180        public CmsObject getCms() {
181
182            return m_cms;
183        }
184
185        /**
186         * The second factor information for 2FA.
187         *
188         * @return the second factor information
189         */
190        public CmsSecondFactorInfo getSecondFactorInfo() {
191
192            return m_secondFactorInfo;
193        }
194
195        /**
196         * Gets the user to be logged in.
197         *
198         * @return the user
199         */
200        public CmsUser getUser() {
201
202            return m_user;
203        }
204
205        /**
206         * Sets the CMS context.
207         *
208         * @param cms the CMS context
209         */
210        public void setCms(CmsObject cms) {
211
212            m_cms = cms;
213        }
214
215        /**
216         * Sets the second factor information for 2FA.
217         *
218         * @param secondFactorInfo the second factor information
219         */
220        public void setSecondFactorInfo(CmsSecondFactorInfo secondFactorInfo) {
221
222            m_secondFactorInfo = secondFactorInfo;
223        }
224
225        /**
226         * Sets the user.
227         *
228         * @param user the user
229         */
230        public void setUser(CmsUser user) {
231
232            m_user = user;
233        }
234
235        /**
236         * @see java.lang.Object#toString()
237         */
238        @Override
239        public String toString() {
240
241            return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
242        }
243    };
244
245    /**
246     * Helper subclass of CmsLoginUserAgreement which can be used without a page context.<p>
247     *
248     * This is only used for detecting whether we need to display the user agreement dialog, not for displaying the dialog itself.<p>
249     */
250    protected static class UserAgreementHelper extends CmsLoginUserAgreement {
251
252        /** The replacement CMS context. */
253        private CmsObject m_cms;
254
255        /** The replacemenet workplace settings. */
256        private CmsWorkplaceSettings m_wpSettings;
257
258        /**
259         * Creates a new instance.<p>
260         *
261         * @param cms the replacement CMS context
262         * @param wpSettings the replacement workplace settings
263         */
264        public UserAgreementHelper(CmsObject cms, CmsWorkplaceSettings wpSettings) {
265
266            super(null);
267            m_cms = cms;
268            m_wpSettings = wpSettings;
269            initAcceptData();
270        }
271
272        /**
273         * @see org.opencms.workplace.CmsWorkplace#getCms()
274         */
275        @Override
276        public CmsObject getCms() {
277
278            return m_cms;
279        }
280
281        /**
282         * @see org.opencms.workplace.CmsWorkplace#getSettings()
283         */
284        @Override
285        public CmsWorkplaceSettings getSettings() {
286
287            return m_wpSettings;
288        }
289
290        /**
291         * @see org.opencms.workplace.CmsWorkplace#initWorkplaceMembers(org.opencms.jsp.CmsJspActionElement)
292         */
293        @Override
294        protected void initWorkplaceMembers(CmsJspActionElement jsp) {
295
296            // do nothing
297        }
298    }
299
300    /**
301     * Helper interface for splitting the actual login off into its own object
302     * which can be called from multiple places.
303     */
304    interface LoginContinuation {
305
306        /**
307         * Continues with the login process.
308         *
309         * @param data the login data
310         * @throws Exception if something goes wrong
311         */
312        void continueLogin(LoginContext data) throws Exception;
313    }
314
315    /** Additional info key to mark accounts as locked due to inactivity. */
316    public static final String KEY_ACCOUNT_LOCKED = "accountLocked";
317
318    /** The logger for this class. */
319    private static final Log LOG = CmsLog.getLog(CmsLoginController.class);
320
321    /** The two-factor authentication handler. */
322    protected CmsTwoFactorAuthenticationHandler m_otpHandler = OpenCms.getTwoFactorAuthenticationHandler();
323
324    /** The UI instance. */
325    CmsLoginUI m_ui;
326
327    /** The administrator CMS context. */
328    private CmsObject m_adminCms;
329
330    /** The parameters collected when the login app was opened. */
331    private LoginParameters m_params;
332
333    /***
334     * Creates a new instance.<p>
335     *
336     * @param adminCms the admin cms context
337     * @param params the parameters for the UI
338     */
339    public CmsLoginController(CmsObject adminCms, LoginParameters params) {
340
341        m_params = params;
342        m_adminCms = adminCms;
343    }
344
345    /**
346     * Returns the link to the login form.<p>
347     *
348     * @param cms the current cms context
349     *
350     * @return the login form link
351     */
352    public static String getFormLink(CmsObject cms) {
353
354        return OpenCms.getLinkManager().substituteLinkForUnknownTarget(
355            cms,
356            CmsWorkplaceLoginHandler.LOGIN_HANDLER,
357            false);
358    }
359
360    /**
361     * Gets the login target link.<p>
362     *
363     * @param currentCms the current CMS context
364     * @param settings the workplace settings
365     * @param requestedResource the requested resource parameter
366     *
367     * @return the login target
368     *
369     * @throws CmsException in case the user has insufficient permissions to access the login target
370     */
371    public static String getLoginTarget(CmsObject currentCms, CmsWorkplaceSettings settings, String requestedResource)
372    throws CmsException {
373
374        String directEditPath = CmsLoginHelper.getDirectEditPath(currentCms, settings.getUserSettings(), false);
375        String target = "";
376        boolean checkRole = false;
377        String fragment = UI.getCurrent() != null ? UI.getCurrent().getPage().getUriFragment() : "";
378        boolean workplace2 = false;
379
380        if ((requestedResource == null) && (directEditPath != null)) {
381            target = directEditPath;
382        } else if ((requestedResource != null) && !CmsWorkplace.JSP_WORKPLACE_URI.equals(requestedResource)) {
383            target = requestedResource;
384        } else {
385            workplace2 = true;
386            target = CmsVaadinUtils.getWorkplaceLink();
387            checkRole = true;
388        }
389
390        UserAgreementHelper userAgreementHelper = new UserAgreementHelper(currentCms, settings);
391        boolean showUserAgreement = userAgreementHelper.isShowUserAgreement();
392        if (showUserAgreement) {
393            target = userAgreementHelper.getConfigurationVfsPath()
394                + "?"
395                + CmsLoginUserAgreement.PARAM_WPRES
396                + "="
397                + target;
398        }
399        if (checkRole && !OpenCms.getRoleManager().hasRole(currentCms, CmsRole.WORKPLACE_USER)) {
400            workplace2 = false;
401            target = CmsLoginHelper.getDirectEditPath(currentCms, settings.getUserSettings(), true);
402            if (target == null) {
403                throw new CmsCustomLoginException(
404                    org.opencms.workplace.Messages.get().container(
405                        org.opencms.workplace.Messages.GUI_LOGIN_FAILED_NO_WORKPLACE_PERMISSIONS_0));
406            }
407        }
408        if (!workplace2) {
409            target = OpenCms.getLinkManager().substituteLink(currentCms, target);
410        }
411
412        if (workplace2 && CmsStringUtil.isEmptyOrWhitespaceOnly(fragment)) {
413            if (settings.getUserSettings().getStartView().startsWith("/")) {
414                if (CmsWorkplace.VIEW_WORKPLACE.equals(settings.getUserSettings().getStartView())) {
415                    fragment = CmsFileExplorerConfiguration.APP_ID;
416                } else if (CmsWorkplace.VIEW_ADMIN.equals(settings.getUserSettings().getStartView())) {
417                    fragment = CmsAppHierarchyConfiguration.APP_ID;
418                }
419            } else {
420                fragment = settings.getUserSettings().getStartView();
421            }
422        }
423
424        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(fragment)) {
425            if (CmsSitemapEditorConfiguration.APP_ID.equals(fragment)) {
426                target = OpenCms.getLinkManager().substituteLink(currentCms, CmsADEManager.PATH_SITEMAP_EDITOR_JSP)
427                    + "?path="
428                    + settings.getUserSettings().getStartFolder();
429            } else {
430                target += "#" + fragment;
431            }
432        }
433        return target;
434    }
435
436    /**
437     * Logs the current user out by invalidating the session an reloading the current URI.<p>
438     * Important:  This works only within vaadin apps.<p>
439     */
440    public static void logout() {
441
442        CmsObject cms = A_CmsUI.getCmsObject();
443        if (UI.getCurrent() instanceof CmsAppWorkplaceUi) {
444            ((CmsAppWorkplaceUi)UI.getCurrent()).onWindowClose();
445        }
446        String loggedInUser = cms.getRequestContext().getCurrentUser().getName();
447        UI.getCurrent().getSession().close();
448        String logoutUri = OpenCms.getLoginManager().getLogoutUri();
449        if (logoutUri != null) {
450            String target = OpenCms.getLinkManager().substituteLinkForUnknownTarget(cms, logoutUri, false);
451            // open in top frame, so it still works when the Vaadin dialog is embedded
452            Page.getCurrent().open(target, "_top", false);
453        } else {
454            String loginLink = OpenCms.getLinkManager().substituteLinkForUnknownTarget(
455                cms,
456                CmsWorkplaceLoginHandler.LOGIN_HANDLER,
457                false);
458            VaadinService.getCurrentRequest().getWrappedSession().invalidate();
459            // open in top frame, so it still works when the Vaadin dialog is embedded
460            Page.getCurrent().open(loginLink, "_top", false);
461            // logout was successful
462            if (LOG.isInfoEnabled()) {
463                LOG.info(
464                    org.opencms.jsp.Messages.get().getBundle().key(
465                        org.opencms.jsp.Messages.LOG_LOGOUT_SUCCESFUL_3,
466                        loggedInUser,
467                        "{workplace logout option}",
468                        cms.getRequestContext().getRemoteAddress()));
469            }
470            CmsUserLog.logLogout(cms);
471        }
472    }
473
474    /**
475     * Logs out the current user redirecting to the login form afterwards.<p>
476     *
477     * @param cms the cms context
478     * @param request the servlet request
479     * @param response the servlet response
480     *
481     * @throws IOException if writing to the response fails
482     */
483    public static void logout(CmsObject cms, HttpServletRequest request, HttpServletResponse response)
484    throws IOException {
485
486        CmsUserLog.logLogout(cms);
487        String loggedInUser = cms.getRequestContext().getCurrentUser().getName();
488        HttpSession session = request.getSession(false);
489        String logoutUri = OpenCms.getLoginManager().getLogoutUri();
490        if (logoutUri == null) {
491            if (session != null) {
492                session.invalidate();
493                /* we need this because a new session might be created after this method,
494                 but before the session info is updated in OpenCmsCore.showResource. */
495                cms.getRequestContext().setUpdateSessionEnabled(false);
496            }
497            // logout was successful
498            if (LOG.isInfoEnabled()) {
499                LOG.info(
500                    org.opencms.jsp.Messages.get().getBundle().key(
501                        org.opencms.jsp.Messages.LOG_LOGOUT_SUCCESFUL_3,
502                        loggedInUser,
503                        cms.getRequestContext().addSiteRoot(cms.getRequestContext().getUri()),
504                        cms.getRequestContext().getRemoteAddress()));
505            }
506            response.sendRedirect(getFormLink(cms));
507        } else {
508            response.sendRedirect(OpenCms.getLinkManager().substituteLinkForUnknownTarget(cms, logoutUri, false));
509        }
510    }
511
512    /**
513     * Gets the PC type.<p>
514     *
515     * @return the PC type
516     */
517    public String getPcType() {
518
519        String result = m_params.getPcType();
520        if (CmsStringUtil.isEmptyOrWhitespaceOnly(result)) {
521            result = "public";
522        }
523        return result;
524    }
525
526    /**
527     * Returns the reset password link.<p>
528     *
529     * @return the reset password link
530     */
531    public String getResetPasswordLink() {
532
533        String result = OpenCms.getLinkManager().substituteLinkForUnknownTarget(
534            CmsLoginUI.m_adminCms,
535            CmsWorkplaceLoginHandler.LOGIN_HANDLER,
536            false) + "?" + CmsLoginHelper.PARAM_RESET_PASSWORD;
537        String ou = m_ui.getOrgUnit();
538        result += "&" + CmsLoginHelper.PARAM_OUFQN + "=" + ou;
539        return result;
540    }
541
542    /**
543     * Returns true if the security option should be displayed in the login dialog.<p>
544     *
545     * @return true if the security option should be displayed in the login dialog
546     */
547    public boolean isShowSecure() {
548
549        return OpenCms.getLoginManager().isEnableSecurity();
550    }
551
552    /**
553     * Called when the user clicks on the 'forgot password' button.<p>
554     */
555    public void onClickForgotPassword() {
556
557        A_CmsUI.get().getPage().setLocation(getResetPasswordLink());
558    }
559
560    /**
561     * Called when the user clicks on the login button.<p>
562     */
563    public void onClickLogin() {
564
565        String user = m_ui.getUser();
566        String password = m_ui.getPassword();
567        String ou = m_ui.getOrgUnit();
568        if (CmsLoginOuSelector.OU_NONE.equals(ou)) {
569            displayError(
570                CmsVaadinUtils.getMessageText(Messages.GUI_LOGIN_NO_OU_SELECTED_WARNING_0) + "\n\n",
571                false,
572                false);
573            return;
574        }
575        {
576            CmsMessageContainer message = CmsLoginHelper.validateUserAndPasswordNotEmpty(user, password);
577            if (message != null) {
578                String errorMessage = message.key(m_params.getLocale());
579                displayError(errorMessage, true, false);
580                return;
581            }
582        }
583
584        String realUser = CmsStringUtil.joinPaths(ou, user);
585        String pcType = m_ui.getPcType();
586        CmsObject currentCms = A_CmsUI.getCmsObject();
587        CmsUser userObj = null;
588
589        try {
590            try {
591                userObj = currentCms.readUser(realUser);
592            } catch (CmsException e) {
593                LOG.warn(e.getLocalizedMessage(), e);
594                CmsMessageContainer message = org.opencms.workplace.Messages.get().container(
595                    org.opencms.workplace.Messages.GUI_LOGIN_FAILED_0);
596                displayError(message.key(m_params.getLocale()), true, true);
597                CmsUserLog.logLoginFailure(currentCms, realUser);
598                return;
599            }
600            final CmsUser userNonNull = userObj;
601            if (OpenCms.getLoginManager().canLockBecauseOfInactivity(currentCms, userObj)) {
602                boolean locked = null != userObj.getAdditionalInfo().get(KEY_ACCOUNT_LOCKED);
603                if (locked) {
604                    displayError(CmsInactiveUserMessages.getLockoutText(A_CmsUI.get().getLocale()), false, false);
605                    CmsUserLog.logLoginFailure(currentCms, realUser);
606                    return;
607                }
608            }
609
610            CmsObject cloneCms = OpenCms.initCmsObject(currentCms);
611            cloneCms.checkLoginUser(realUser, password);
612
613            String messageToChange = "";
614            if (OpenCms.getLoginManager().isPasswordReset(currentCms, userObj)) {
615                messageToChange = CmsVaadinUtils.getMessageText(Messages.GUI_PWCHANGE_RESET_0);
616            }
617            if (OpenCms.getLoginManager().requiresPasswordChange(currentCms, userObj)) {
618                messageToChange = getPasswordChangeMessage();
619            }
620            if (!CmsStringUtil.isEmptyOrWhitespaceOnly(messageToChange)) {
621                CmsChangePasswordDialog passwordDialog = new CmsChangePasswordDialog(
622                    currentCms,
623                    userObj,
624                    A_CmsUI.get().getLocale());
625                passwordDialog.setAdditionalMessage(messageToChange);
626                A_CmsUI.get().setContentToDialog(
627                    Messages.get().getBundle(A_CmsUI.get().getLocale()).key(Messages.GUI_PWCHANGE_HEADER_0)
628                        + userObj.getSimpleName(),
629                    passwordDialog);
630                return;
631            }
632
633            /*
634             * Login branches now into multiple paths: For the non-OTP case we can do it directly (synchronously),
635             * and for the OTP cases we need to show additional dialogs first (so it's asynchronous). But at the end,
636             * the same things happen. So we put these 'things that happen at the end' in a closure, so we can either
637             * execute it directly or pass it as a callback to the dialogs.
638             */
639            LoginContinuation loginContinuation = (LoginContext details) -> {
640
641                // provisional login successful, now do for real
642                // we use another separate CmsObject so we can manually control when it is written to the session info
643                CmsObject loginCms = OpenCms.initCmsObject(currentCms);
644                CmsSecondFactorInfo secondFactorInfo = details.getSecondFactorInfo();
645                loginCms.loginUser(realUser, password, secondFactorInfo);
646                CmsUserLog.logLogin(loginCms, realUser);
647
648                if (LOG.isInfoEnabled()) {
649                    CmsRequestContext context = loginCms.getRequestContext();
650                    LOG.info(
651                        org.opencms.jsp.Messages.get().getBundle().key(
652                            org.opencms.jsp.Messages.LOG_LOGIN_SUCCESSFUL_3,
653                            context.getCurrentUser().getName(),
654                            "{workplace login dialog}",
655                            context.getRemoteAddress()));
656                }
657                CmsWorkplaceSettings settings = CmsLoginHelper.initSiteAndProject(loginCms);
658                final String loginTarget = getLoginTarget(loginCms, settings, m_params.getRequestedResource());
659
660                CmsLoginHelper.setCookieData(
661                    pcType,
662                    user,
663                    ou,
664                    (VaadinServletRequest)(VaadinService.getCurrentRequest()),
665                    (VaadinServletResponse)(VaadinService.getCurrentResponse()));
666                VaadinService.getCurrentRequest().getWrappedSession().setAttribute(
667                    CmsWorkplaceManager.SESSION_WORKPLACE_SETTINGS,
668                    settings);
669
670                final boolean isPublicPC = CmsLoginForm.PC_TYPE_PUBLIC.equals(pcType);
671                if (OpenCms.getLoginManager().requiresUserDataCheck(loginCms, userNonNull)) {
672                    I_CmsDialogContext context = new A_CmsDialogContext("", ContextType.appToolbar, null) {
673
674                        @Override
675                        public void finish(CmsProject project, String siteRoot) {
676
677                            finish(null);
678                        }
679
680                        @Override
681                        public void finish(Collection<CmsUUID> result) {
682
683                            initSessionAndSendMessages(currentCms, loginCms);
684                            m_ui.openLoginTarget(loginTarget, isPublicPC);
685
686                        }
687
688                        public void focus(CmsUUID structureId) {
689
690                            // nothing to do
691                        }
692
693                        public List<CmsUUID> getAllStructureIdsInView() {
694
695                            return null;
696                        }
697
698                        @Override
699                        public CmsObject getCms() {
700
701                            return loginCms;
702                        }
703
704                        @Override
705                        public void start(String title, Component dialog, DialogWidth style) {
706
707                            if (dialog != null) {
708                                m_window = CmsBasicDialog.prepareWindow(style);
709                                m_window.setCaption(title);
710                                m_window.setContent(dialog);
711                                UI.getCurrent().addWindow(m_window);
712                                if (dialog instanceof CmsBasicDialog) {
713                                    ((CmsBasicDialog)dialog).initActionHandler(m_window);
714                                }
715                            }
716                        }
717
718                        public void updateUserInfo() {
719
720                            // not supported
721                        }
722                    };
723                    CmsUser u = currentCms.readUser(userNonNull.getId());
724                    u.setAdditionalInfo(
725                        CmsUserSettings.ADDITIONAL_INFO_LAST_USER_DATA_CHECK,
726                        Long.toString(System.currentTimeMillis()));
727                    loginCms.writeUser(u);
728                    CmsUserDataDialog dialog = new CmsUserDataDialog(context, true);
729                    context.start(dialog.getTitle(UI.getCurrent().getLocale()), dialog);
730                } else {
731                    initSessionAndSendMessages(currentCms, loginCms);
732                    m_ui.openLoginTarget(loginTarget, isPublicPC);
733                }
734            };
735            LoginContext context = new LoginContext();
736            context.setUser(userNonNull);
737            context.setCms(currentCms);
738            if (m_otpHandler.needsTwoFactorAuthentication(userNonNull)) {
739                m_ui.clearError();
740                if (!m_otpHandler.hasSecondFactor(userObj)) {
741                    showSecondFactorSetup(context, loginContinuation);
742                } else {
743                    showSecondFactorDialog(context, loginContinuation);
744                }
745            } else {
746                loginContinuation.continueLogin(context);
747            }
748        } catch (Exception e) {
749
750            handleError(currentCms, realUser, e);
751        }
752    }
753
754    /**
755     * Called on initialization.<p>
756     */
757    public void onInit() {
758
759        String authToken = m_params.getAuthToken();
760        if (authToken != null) {
761            m_ui.showForgotPasswordView(authToken);
762        } else if (m_params.isReset()) {
763            m_ui.showPasswordResetDialog(m_params.getOufqn());
764        } else {
765            boolean loggedIn = !A_CmsUI.getCmsObject().getRequestContext().getCurrentUser().isGuestUser();
766            m_ui.setSelectableOrgUnits(CmsLoginHelper.getOrgUnitsForLoginDialog(A_CmsUI.getCmsObject(), null));
767            if (loggedIn) {
768                if (m_params.isLogout()) {
769                    logout();
770                } else {
771                    m_ui.showAlreadyLoggedIn();
772                }
773            } else {
774                m_ui.showLoginView(m_params.getOufqn());
775            }
776        }
777
778    }
779
780    /**
781     * Sets the login ui reference.<p>
782     *
783     * @param ui the login ui
784     */
785    public void setUi(CmsLoginUI ui) {
786
787        m_ui = ui;
788    }
789
790    /**
791     * Returns the message to be displayed for the user data check dialog.<p>
792     *
793     * @return the message to display
794     */
795    protected String getPasswordChangeMessage() {
796
797        ResourceBundle bundle = null;
798        try {
799            bundle = CmsResourceBundleLoader.getBundle("org.opencms.passwordchange.custom", A_CmsUI.get().getLocale());
800            return bundle.getString("passwordchange.text");
801        } catch (MissingResourceException e) {
802            return CmsVaadinUtils.getMessageText(Messages.GUI_PWCHANGE_INTERVAL_EXPIRED_0);
803        }
804    }
805
806    /**
807     * Handles exceptions during the login process and displays appropriate error messages.
808     *
809     * @param currentCms the CMS context
810     * @param user the user being logged in
811     * @param e the error
812     */
813    protected void handleError(CmsObject currentCms, String user, Exception e) {
814
815        CmsMessageContainer message = null;
816
817        // there was an error during login
818        if (e instanceof CmsException) {
819            CmsMessageContainer exceptionMessage = ((CmsException)e).getMessageContainer();
820            if (org.opencms.security.Messages.ERR_LOGIN_FAILED_DISABLED_2 == exceptionMessage.getKey()) {
821                // the user account is disabled
822                message = org.opencms.workplace.Messages.get().container(
823                    org.opencms.workplace.Messages.GUI_LOGIN_FAILED_DISABLED_0);
824            } else if (org.opencms.security.Messages.ERR_LOGIN_FAILED_TEMP_DISABLED_4 == exceptionMessage.getKey()) {
825                // the user account is temporarily disabled because of too many login failures
826                message = org.opencms.workplace.Messages.get().container(
827                    org.opencms.workplace.Messages.GUI_LOGIN_FAILED_TEMP_DISABLED_0);
828            } else if (org.opencms.security.Messages.ERR_LOGIN_FAILED_WITH_MESSAGE_1 == exceptionMessage.getKey()) {
829                // all logins have been disabled be the Administration
830                CmsLoginMessage loginMessage2 = OpenCms.getLoginManager().getLoginMessage();
831                if (loginMessage2 != null) {
832                    message = org.opencms.workplace.Messages.get().container(
833                        org.opencms.workplace.Messages.GUI_LOGIN_FAILED_WITH_MESSAGE_1,
834                        loginMessage2.getMessage().replace("\n", ""));
835                }
836            }
837        }
838        if (message == null) {
839            if (e instanceof CmsCustomLoginException) {
840                message = ((CmsCustomLoginException)e).getMessageContainer();
841            } else {
842                // any other error - display default message
843                message = org.opencms.workplace.Messages.get().container(
844                    org.opencms.workplace.Messages.GUI_LOGIN_FAILED_0);
845                LOG.warn(e.getLocalizedMessage(), e);
846                displayError(message.key(m_params.getLocale()), true, true);
847                CmsUserLog.logLoginFailure(currentCms, user);
848                return;
849            }
850        }
851
852        if (e instanceof CmsException) {
853            CmsJspLoginBean.logLoginException(currentCms.getRequestContext(), user, (CmsException)e);
854            CmsUserLog.logLoginFailure(currentCms, user);
855        } else {
856            LOG.error(e.getLocalizedMessage(), e);
857        }
858        displayError(message.key(m_params.getLocale()), false, false);
859    }
860
861    /**
862     * Switches the session to a new one with the logged in CmsObject.
863     *
864     * <p>This needs to be called in the <em>last</em> request to the Vaadin servlet in the login process, because switching the session breaks the Vaadin session state.
865     *
866     * @param currentCms the CmsObject for the current request from the Vaadin UI
867     * @param loginCms the CmsObject which was used for the actual login operation
868     */
869    protected void initSessionAndSendMessages(CmsObject currentCms, CmsObject loginCms) {
870
871        HttpSession session = ((HttpServletRequest)VaadinService.getCurrentRequest()).getSession(false);
872        if (session != null) {
873            session.invalidate();
874        }
875        session = ((HttpServletRequest)VaadinService.getCurrentRequest()).getSession(true);
876
877        // we don't want currentCms to be used to automatically update the session at the end of the request...
878        currentCms.getRequestContext().setUpdateSessionEnabled(false);
879
880        // ...instead we manually update the session with loginCms
881        loginCms.getRequestContext().setUpdateSessionEnabled(true);
882        OpenCms.getSessionManager().updateSessionInfo(loginCms, (HttpServletRequest)VaadinService.getCurrentRequest());
883
884        String storedMessage = null;
885        CmsLoginMessage loginMessage = OpenCms.getLoginManager().getLoginMessage();
886        if (loginMessage != null) {
887            // forbidden implies active
888            if (loginMessage.isLoginCurrentlyForbidden()) {
889                // we are an administrator, otherwise login would have failed
890                if (loginMessage.getTimeEnd() == CmsLoginMessage.DEFAULT_TIME_END) {
891                    storedMessage = org.opencms.workplace.Messages.get().container(
892                        org.opencms.workplace.Messages.GUI_LOGIN_SUCCESS_WITH_MESSAGE_WITHOUT_TIME_1,
893                        loginMessage.getMessage(),
894                        new Date(loginMessage.getTimeEnd())).key(A_CmsUI.get().getLocale());
895                } else {
896                    storedMessage = org.opencms.workplace.Messages.get().container(
897                        org.opencms.workplace.Messages.GUI_LOGIN_SUCCESS_WITH_MESSAGE_2,
898                        loginMessage.getMessage(),
899                        new Date(loginMessage.getTimeEnd())).key(A_CmsUI.get().getLocale());
900                }
901            } else if (loginMessage.isActive()) {
902                storedMessage = loginMessage.getMessage();
903            }
904        }
905
906        if (storedMessage != null) {
907            OpenCms.getSessionManager().sendBroadcast(
908                null,
909                storedMessage,
910                loginCms.getRequestContext().getCurrentUser(),
911                ContentMode.html);
912        }
913    }
914
915    /**
916     * Gets the CMS context.<p>
917     *
918     * @return the CMS context
919     */
920    CmsObject getCms() {
921
922        return m_adminCms;
923    }
924
925    /**
926     * Displays the given error message.<p>
927     *
928     * @param message the message
929     * @param showForgotPassword in case the forgot password link should be shown
930     * @param showTime show the time
931     */
932    private void displayError(String message, boolean showForgotPassword, boolean showTime) {
933
934        message = message.replace("\n", "<br />");
935        if (showForgotPassword) {
936            message += "<br /><br /><a href=\""
937                + getResetPasswordLink()
938                + "\">"
939                + CmsVaadinUtils.getMessageText(Messages.GUI_FORGOT_PASSWORD_0)
940                + "</a>";
941        }
942        if (showTime) {
943            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
944            message += "<div style=\"position:absolute;right:6px;bottom:5px;\">"
945                + CmsVaadinUtils.getMessageText(Messages.GUI_TIME_1, sdf.format(new Date()))
946                + "</div>";
947        }
948        m_ui.showLoginError(message);
949    }
950
951    /**
952     * Shows the verification dialog for 2FA.
953     *
954     * @param context the login context
955     * @param loginContinuation the handler to which we pass the verification code to continue with the login
956     */
957    private void showSecondFactorDialog(LoginContext context, LoginContinuation loginContinuation) {
958
959        Window window = CmsBasicDialog.prepareWindow(DialogWidth.narrow);
960        window.setClosable(false);
961        window.setResizable(false);
962        window.setCaption(CmsSecondFactorDialog.getCaption(context.getUser()));
963        CmsSecondFactorDialog dialog = new CmsSecondFactorDialog(context.getUser(), verificationCode -> {
964            context.setSecondFactorInfo(new CmsSecondFactorInfo(verificationCode));
965            try {
966                loginContinuation.continueLogin(context);
967            } catch (Exception e) {
968                handleError(context.getCms(), context.getUser().getName(), e);
969            }
970        });
971        A_CmsUI.get().addWindow(window);
972        window.setContent(dialog);
973
974    }
975
976    /**
977     * Shows a dialog for setting up 2FA.
978     *
979     * @param context the login context
980     * @param loginContinuation the handler we call with the secret and verification code to set up 2FA and proceed with the loin.
981     */
982    private void showSecondFactorSetup(LoginContext context, LoginContinuation loginContinuation) {
983
984        Window window = CmsBasicDialog.prepareWindow(DialogWidth.wide);
985        window.setClosable(false);
986        window.setResizable(false);
987        CmsSecondFactorSetupDialog dialog = new CmsSecondFactorSetupDialog(context, context2 -> {
988            try {
989                loginContinuation.continueLogin(context);
990            } catch (Exception e) {
991                handleError(context.getCms(), context.getUser().getName(), e);
992            }
993        });
994        window.setCaption(CmsVaadinUtils.getMessageText(Messages.GUI_LOGIN_2FA_SETUP_0));
995        A_CmsUI.get().addWindow(window);
996        window.setContent(dialog);
997
998    }
999}