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.jsp.userdata;
029
030import org.opencms.ade.detailpage.CmsDetailPageInfo;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsProperty;
033import org.opencms.file.CmsPropertyDefinition;
034import org.opencms.file.CmsResource;
035import org.opencms.file.CmsUser;
036import org.opencms.i18n.CmsEncoder;
037import org.opencms.i18n.CmsMessages;
038import org.opencms.jsp.util.CmsJspStandardContextBean;
039import org.opencms.main.CmsException;
040import org.opencms.main.CmsLog;
041import org.opencms.main.OpenCms;
042import org.opencms.security.CmsAuthentificationException;
043import org.opencms.security.CmsOrganizationalUnit;
044import org.opencms.util.CmsCollectionsGenericWrapper;
045import org.opencms.util.CmsStringUtil;
046
047import java.util.Collections;
048import java.util.HashMap;
049import java.util.List;
050import java.util.Map;
051import java.util.Optional;
052
053import javax.mail.internet.AddressException;
054
055import org.apache.commons.logging.Log;
056import org.apache.commons.mail.EmailException;
057
058/**
059 * Bean used by the dynamic function JSP for user data requests.<p>
060 *
061 * The dynamic function calls the action() method on this bean, which returns the new state for the JSP to render.
062 * Error/status messages are also available to the JSP via getters.
063 */
064public class CmsJspUserDataRequestBean {
065
066    /**
067     * Represents the UI state which should be shown by the dynamic function JSP.
068     */
069    enum State {
070        /** Generic error occurred that can't be meaningfully displayed in another state. */
071        error,
072
073        /** State showing the data request form with user / email / password fields, and possibly an error message. */
074        form,
075        /** Success state after submitting the form. */
076        formOk,
077
078        /** State which is shown when the link from the email is opened. */
079        view,
080
081        /** State which is shown when the user confirms their credentials after clicking on the email link. */
082        viewOk
083    }
084
085    /** Action id. */
086    public static final String ACTION_REQUEST = "request";
087
088    /** Action id. */
089    public static final String ACTION_VIEW = "view";
090
091    /** Action id. */
092    public static final String ACTION_VIEWAUTH = "viewauth";
093
094    /** Request parameter for the action. */
095    public static final String PARAM_ACTION = "action";
096
097    /** Request parameter for the authorization code. */
098    public static final String PARAM_AUTH = "auth";
099
100    /** Request parameter. */
101    public static final String PARAM_EMAIL = "email";
102
103    /** Request parameter. */
104    public static final String PARAM_PASSWORD = "password";
105
106    /** Request parameter. */
107    public static final String PARAM_ROOTPATH = "rootpath";
108
109    /** Request parameter. */
110    public static final String PARAM_UDRID = "udrid";
111
112    /** Request parameter. */
113    public static final String PARAM_USER = "user";
114
115    /** The logger instance for the class. */
116    private static final Log LOG = CmsLog.getLog(CmsJspUserDataRequestBean.class);
117
118    /** The configuration. */
119    private CmsUserDataRequestConfig m_config;
120
121    /** The download link (only available if credentials have been confirmed). */
122    private String m_downloadLink;
123
124    /** Error HTML (only shown directly in error state). */
125    private String m_errorHtml;
126
127    /** The user data request info. */
128    private CmsUserDataRequestInfo m_info;
129
130    /** The user data HTML (only available if credentials have been confirmed). */
131    private String m_infoHtml;
132
133    /** The request parameters (duplicate parameters are removed). */
134    private Map<String, String> m_params;
135
136    /** Lazy map to access messages. */
137    private Map<String, String> m_texts;
138
139    /**
140     * Creates a new instance.
141     */
142    public CmsJspUserDataRequestBean() {
143
144        LOG.debug("Creating user data request bean.");
145    }
146
147    /**
148     * Called by the user data request function JSP to handle the user data request logic.
149     *
150     *  Returns the next state which should be shown by the JSP.
151     *
152     * @param cms the CMS context
153     * @param reqParameters the request parameters
154     * @return the next state to render
155     *
156     * @throws CmsException if something goes wrong
157     */
158    public String action(CmsObject cms, Map<String, String[]> reqParameters) throws CmsException {
159
160        init(reqParameters);
161
162        CmsResource page = cms.readResource(cms.getRequestContext().getUri());
163        List<CmsProperty> props = cms.readPropertyObjects(page, true);
164
165        CmsProperty defaultOuProp = CmsProperty.get(CmsPropertyDefinition.PROPERTY_UDR_DEFAULTOU, props);
166        String configuredOu = null;
167        if (defaultOuProp != null) {
168
169            configuredOu = defaultOuProp.getValue();
170        }
171        CmsProperty configPathProp = CmsProperty.get(CmsPropertyDefinition.PROPERTY_UDR_CONFIG, props);
172        String configPath = configPathProp.getValue();
173
174        CmsUserDataRequestManager manager = OpenCms.getUserDataRequestManager();
175        CmsMessages messages = Messages.get().getBundle(cms.getRequestContext().getLocale());
176        m_config = manager.loadConfig(cms, configPath).orElse(null);
177        if (m_config == null) {
178            m_errorHtml = messages.key(Messages.ERR_CONFIG_NOT_SET_0);
179            return State.error.toString();
180        }
181        String functionDetail = CmsJspStandardContextBean.getFunctionDetailLink(
182            cms,
183            CmsDetailPageInfo.FUNCTION_PREFIX,
184            CmsUserDataRequestManager.FUNCTION_NAME,
185            true);
186        if (functionDetail.contains("[")) {
187            m_errorHtml = messages.key(Messages.ERR_FUNCTION_DETAIL_PAGE_NOT_SET_0);
188            return State.error.toString();
189        }
190
191        if (!CmsUserDataResourceHandler.isInitialized()) {
192            m_errorHtml = messages.key(Messages.ERR_RESOURCE_INIT_HANDLER_NOT_CONFIGURED_0);
193            return State.error.toString();
194        }
195        String email = m_params.get(PARAM_EMAIL);
196        String user = m_params.get(PARAM_USER);
197        String password = m_params.get(PARAM_PASSWORD);
198        String path = m_params.get(PARAM_ROOTPATH);
199        String udrid = CmsEncoder.escapeXml(m_params.get(PARAM_UDRID));
200        boolean hasEmail = !CmsStringUtil.isEmptyOrWhitespaceOnly(email);
201        boolean hasUser = !CmsStringUtil.isEmptyOrWhitespaceOnly(user);
202        boolean hasPassword = !CmsStringUtil.isEmptyOrWhitespaceOnly(password);
203        String action = CmsEncoder.escapeXml(m_params.get(PARAM_ACTION));
204        if (!CmsStringUtil.isEmpty(udrid)) {
205            m_info = manager.getRequestStore().load(udrid).orElse(null);
206        }
207        if (action == null) {
208            return State.form.toString();
209        } else if (ACTION_REQUEST.equals(action)) {
210            try {
211                if (hasEmail && !hasUser && !hasPassword) {
212                    manager.startUserDataRequest(cms, m_config, email);
213                    return State.formOk.toString();
214                } else if (!hasEmail && hasUser && hasPassword) {
215                    if (CmsStringUtil.isEmpty(path)) {
216                        path = cms.getRequestContext().addSiteRoot(cms.getRequestContext().getUri());
217                    } else {
218                        path = path.trim();
219                    }
220                    Optional<CmsUser> optUser = lookupUser(cms, configuredOu, path, user, password);
221                    if (optUser.isPresent()) {
222                        manager.startUserDataRequest(cms, m_config, optUser.get());
223                        return State.formOk.toString();
224                    } else {
225                        throw new CmsUserDataRequestException("Could not find user.");
226                    }
227                } else {
228                    m_errorHtml = "invalid combination of parameters.";
229                    return State.form.toString();
230                }
231            } catch (CmsUserDataRequestException e) {
232                m_errorHtml = e.getLocalizedMessage();
233                return State.form.toString();
234            } catch (EmailException | AddressException e) {
235                LOG.warn(e.getLocalizedMessage(), e);
236                m_errorHtml = m_config.getText("EmailError");
237                return State.error.toString();
238            }
239        } else if (ACTION_VIEW.equals(action)) {
240            CmsUserDataRequestInfo info = manager.getRequestStore().load(udrid).orElse(null);
241            if ((info == null) || info.isExpired()) {
242                m_errorHtml = m_config.getText("InvalidLink");
243                return State.error.toString();
244            }
245            return State.view.toString();
246        } else if (ACTION_VIEWAUTH.equals(action)) {
247            CmsUserDataRequestStore store = manager.getRequestStore();
248            CmsUserDataRequestInfo info = store.load(udrid).orElse(null);
249            if ((info == null) || info.isExpired()) {
250                m_errorHtml = m_config.getText("InvalidLink");
251                return State.error.toString();
252            }
253            m_infoHtml = info.getInfoHtml();
254            boolean origForceAbsolute = cms.getRequestContext().isForceAbsoluteLinks();
255            cms.getRequestContext().setForceAbsoluteLinks(true);
256            try {
257                m_downloadLink = OpenCms.getLinkManager().substituteLinkForUnknownTarget(
258                    cms,
259                    CmsUserDataResourceHandler.PREFIX + info.getId() + "?" + PARAM_AUTH + "=" + info.getAuthCode());
260            } finally {
261                cms.getRequestContext().setForceAbsoluteLinks(origForceAbsolute);
262            }
263            CmsUserDataRequestType type = info.getType();
264            if (CmsUserDataRequestType.email.equals(type)) {
265                if (email == null) {
266                    return State.view.toString();
267                } else if (email.equals(info.getEmail())) {
268                    return State.viewOk.toString();
269                } else {
270                    m_errorHtml = "email address error";
271                    return State.view.toString();
272                }
273            } else if (CmsUserDataRequestType.singleUser.equals(type)) {
274                if (hasUser && hasPassword) {
275                    try {
276                        CmsObject cloneCms = OpenCms.initCmsObject(cms);
277                        String fullName = info.getUserName();
278                        CmsUser readUser = cms.readUser(fullName);
279                        if (!readUser.getSimpleName().equals(user)) {
280                            m_errorHtml = "login error";
281                            return State.view.toString();
282                        }
283                        cloneCms.loginUser(info.getUserName(), password);
284                        return State.viewOk.toString();
285                    } catch (CmsException e) {
286                        LOG.info(e.getLocalizedMessage(), e);
287                        m_errorHtml = "login error";
288                        return State.view.toString();
289                    }
290
291                }
292                m_errorHtml = "authentication error";
293                return State.view.toString();
294            }
295
296        } else {
297            m_errorHtml = "Invalid action: " + action;
298            LOG.info("Invalid action: " + action);
299            return State.error.toString();
300        }
301        return State.form.toString();
302
303    }
304
305    /**
306     * Gets the user data download link.
307     *
308     * @return the user data download link
309     */
310    public String getDownloadLink() {
311
312        return m_downloadLink;
313    }
314
315    /**
316     * Gets the error HTML (this is only shown in the error state).
317     *
318     * @return the error HTML
319     */
320    public String getErrorHtml() {
321
322        return m_errorHtml;
323    }
324
325    /**
326     * Gets the user data HTML.<p>
327     *
328     * @return the user data HTML
329     */
330    public String getInfoHtml() {
331
332        return m_infoHtml;
333    }
334
335    /**
336     * Gets the lazy map used to access the configurable texts to display by the user data request dynamic function.
337     *
338     * @return the lazy map
339     */
340    public Map<String, String> getTexts() {
341
342        if (m_texts == null) {
343            m_texts = CmsCollectionsGenericWrapper.createLazyMap(obj -> {
344                String key = (String)obj;
345                return m_config.getText(key);
346
347            });
348        }
349        return m_texts;
350
351    }
352
353    /**
354     * Checks if the error HTML has been set.
355     *
356     * @return true if the error HTML has been set
357     */
358    public boolean isError() {
359
360        return m_errorHtml != null;
361    }
362
363    /**
364     * Checks if we need to confirm the user's email rather than their user name and password.
365     *
366     * @return true if we only need the email address
367     */
368    public boolean isOnlyEmailRequired() {
369
370        return CmsUserDataRequestType.email == m_info.getType();
371    }
372
373    /**
374     * Initializes the request parameters by throwing out duplicates.
375     *
376     * @param requestParams the request parameters
377     */
378    private void init(Map<String, String[]> requestParams) {
379
380        m_params = new HashMap<>();
381        for (Map.Entry<String, String[]> entry : requestParams.entrySet()) {
382            String[] vals = entry.getValue();
383            if (vals.length > 0) {
384                String val = vals[0];
385                m_params.put(entry.getKey(), val);
386            }
387        }
388    }
389
390    /**
391     * Looks up the 'best' user for the given user name and password.
392     *
393     * <p>The 'best' in this case is found by walking up the OUs, from the most specific to the root OU, which contain the current URI
394     * and trying to log in the user with the given name and OU in each of them. The first OU+User combination for which the login is successful
395     * is returned.
396     *
397     * @param cms the CMS context
398     * @param configuredOu the configured OU
399     * @param path the path to use for looking up the OU if the OU is not given
400     * @param user the user
401     * @param password the password
402     *
403     * @return the found user
404     *
405     * @throws CmsException if something goes wrong
406     */
407    private Optional<CmsUser> lookupUser(CmsObject cms, String configuredOu, String path, String user, String password)
408    throws CmsException {
409
410        List<CmsOrganizationalUnit> ous;
411        if (configuredOu != null) {
412            ous = Collections.singletonList(OpenCms.getOrgUnitManager().readOrganizationalUnit(cms, configuredOu));
413        } else {
414            ous = OpenCms.getOrgUnitManager().getOrgUnitsForResource(cms, path);
415        }
416        CmsObject loginCms = OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserGuest());
417        for (CmsOrganizationalUnit ou : ous) {
418            String fullName = CmsStringUtil.joinPaths(ou.getName(), user);
419            try {
420                loginCms.loginUser(fullName, password);
421                return Optional.of(loginCms.getRequestContext().getCurrentUser());
422            } catch (CmsAuthentificationException e) {
423                LOG.debug(e.getLocalizedMessage(), e);
424            }
425        }
426        return Optional.empty();
427    }
428
429}