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.workplace;
029
030import org.opencms.cache.CmsVfsMemoryObjectCache;
031import org.opencms.db.CmsUserSettings;
032import org.opencms.file.CmsFile;
033import org.opencms.file.CmsPropertyDefinition;
034import org.opencms.file.CmsUser;
035import org.opencms.json.JSONException;
036import org.opencms.json.JSONObject;
037import org.opencms.jsp.CmsJspActionElement;
038import org.opencms.main.CmsException;
039import org.opencms.main.CmsLog;
040import org.opencms.ui.CmsVaadinUtils;
041import org.opencms.util.CmsStringUtil;
042import org.opencms.xml.content.CmsXmlContent;
043import org.opencms.xml.content.CmsXmlContentFactory;
044
045import java.io.IOException;
046
047import javax.servlet.http.HttpServletRequest;
048import javax.servlet.http.HttpServletResponse;
049import javax.servlet.jsp.PageContext;
050
051import org.apache.commons.logging.Log;
052
053/**
054 * Provides methods to show a configurable user agreement dialog after a successful workplace login.<p>
055 *
056 * @since 8.0
057 */
058public class CmsLoginUserAgreement extends CmsDialog {
059
060    /** Value for the action: accept the user agreement. */
061    public static final int ACTION_ACCEPT = 100;
062
063    /** The dialog type. */
064    public static final String DIALOG_TYPE = "useragreement";
065
066    /** Node name for the element: MessageDeclined. */
067    public static final String NODE_MESSAGE_DECLINED = "MessageDeclined";
068
069    /** Node name for the element: Text. */
070    public static final String NODE_TEXT = "Text";
071
072    /** Request parameter name for the originally requested resource. */
073    public static final String PARAM_WPRES = "wpres";
074
075    /** JSON key name to store the count of the accepted agreement. */
076    protected static final String KEY_ACCEPTED_COUNT = "count";
077
078    /** JSON key name to store the version of the accepted agreement. */
079    protected static final String KEY_ACCEPTED_VERSION = "version";
080
081    /** Node name for the element: AgreeCount. */
082    protected static final String NODE_AGREE_COUNT = "AgreeCount";
083
084    /** Node name for the element: ButtonAccept. */
085    protected static final String NODE_BUTTON_ACCEPT = "ButtonAccept";
086
087    /** Node name for the element: ButtonDecline. */
088    protected static final String NODE_BUTTON_DECLINE = "ButtonDecline";
089
090    /** Node name for the element: DialogTitle. */
091    protected static final String NODE_DIALOG_TITLE = "DialogTitle";
092
093    /** Node name for the element: Version. */
094    protected static final String NODE_VERSION = "Version";
095
096    /** The VFS path to the folder containing the user agreement configuration files. */
097    protected static final String VFS_PATH_CONFIGFOLDER = CmsWorkplace.VFS_PATH_SYSTEM + "login/useragreement/";
098
099    /** The log object for this class. */
100    private static final Log LOG = CmsLog.getLog(CmsLoginUserAgreement.class);
101
102    /** The number of times the user accepted the agreement. */
103    private int m_acceptedCount;
104
105    /** The version of the user accepted agreement. */
106    private double m_acceptedVersion;
107
108    /** The user agreement configuration content. */
109    private CmsXmlContent m_configurationContent;
110
111    /** The originally requested workplace resource path parameter. */
112    private String m_paramWpres;
113
114    /** The required version of the user accepted agreement. */
115    private double m_requiredVersion;
116
117    /**
118     * Public constructor with JSP action element.<p>
119     *
120     * @param jsp an initialized JSP action element
121     */
122    public CmsLoginUserAgreement(CmsJspActionElement jsp) {
123
124        super(jsp);
125    }
126
127    /**
128     * Public constructor with JSP variables.<p>
129     *
130     * @param context the JSP page context
131     * @param req the JSP request
132     * @param res the JSP response
133     */
134    public CmsLoginUserAgreement(PageContext context, HttpServletRequest req, HttpServletResponse res) {
135
136        this(new CmsJspActionElement(context, req, res));
137    }
138
139    /**
140     * Stores the information about the accepted user agreement in the current users additional info.<p>
141     */
142    public void acceptAgreement() {
143
144        // store accepted flag in current users settings
145        getSettings().setUserAgreementAccepted(true);
146        // check accepted agreement version
147        if (getAcceptedVersion() < getRequiredVersion()) {
148            // a new (higher) version was accepted, increase version to store and reset accepted count
149            setAcceptedVersion(getRequiredVersion());
150            setAcceptedCount(0);
151        }
152        // increase accepted count
153        setAcceptedCount(getAcceptedCount() + 1);
154        // create the JSON data structure that is stored in the additional info
155        JSONObject jsonData = new JSONObject();
156        try {
157            jsonData.put(KEY_ACCEPTED_VERSION, getRequiredVersion());
158            jsonData.put(KEY_ACCEPTED_COUNT, m_acceptedCount);
159            // store accepted data in users additional information
160            CmsUser user = getCms().getRequestContext().getCurrentUser();
161            user.setAdditionalInfo(CmsUserSettings.LOGIN_USERAGREEMENT_ACCEPTED, jsonData.toString());
162            // write the changed user
163            getCms().writeUser(user);
164        } catch (Exception e) {
165            LOG.error(e.getLocalizedMessage(), e);
166        }
167    }
168
169    /**
170     * Performs the the user agreement accept action, will be called by the JSP page.<p>
171     *
172     * @throws IOException if problems while redirecting occur
173     */
174    public void actionAccept() throws IOException {
175
176        acceptAgreement();
177        // redirect to the originally requested resource
178        getJsp().getResponse().sendRedirect(getJsp().link(getParamWpres()));
179    }
180
181    /**
182     * Performs the user agreement declined action, will be called by the JSP page.<p>
183     *
184     * @throws IOException if problems while redirecting occur
185     */
186    public void actionDecline() throws IOException {
187
188        getJsp().getRequest().getSession().invalidate();
189        getJsp().getResponse().sendRedirect(getJsp().link(getParamWpres()));
190    }
191
192    /**
193     * The standard JavaScript for submitting the dialog is overridden to show an alert in case that
194     * an agreement is declined.<p>
195     *
196     * See also {@link CmsDialog#dialogScriptSubmit()}
197     *
198     * @return the standard JavaScript for submitting the dialog
199     */
200    @Override
201    public String dialogScriptSubmit() {
202
203        if (useNewStyle()) {
204            return super.dialogScriptSubmit();
205        }
206        StringBuffer result = new StringBuffer(512);
207        result.append("function submitAction(actionValue, theForm, formName) {\n");
208        result.append("\tif (theForm == null) {\n");
209        result.append("\t\ttheForm = document.forms[formName];\n");
210        result.append("\t}\n");
211        result.append("\ttheForm." + PARAM_FRAMENAME + ".value = window.name;\n");
212        result.append("\tif (actionValue == \"" + DIALOG_OK + "\") {\n");
213        result.append("\t\treturn true;\n");
214        result.append("\t}");
215        String declinedMessage = getConfigurationContentStringValue(NODE_MESSAGE_DECLINED);
216        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(declinedMessage)) {
217            // add the alert to show the declined message
218            result.append(" else if (actionValue == \"" + DIALOG_CANCEL + "\") {\n");
219            result.append("\t\talert(\"");
220            result.append(CmsStringUtil.escapeJavaScript(declinedMessage));
221            result.append("\");\n");
222            result.append("\t}\n");
223        }
224        result.append("\ttheForm." + PARAM_ACTION + ".value = actionValue;\n");
225        result.append("\ttheForm.submit();\n");
226        result.append("\treturn false;\n");
227        result.append("}\n");
228
229        return result.toString();
230    }
231
232    /**
233     * Returns the number of times the user accepted the agreement.<p>
234     *
235     * @return the number of times the user accepted the agreement
236     */
237    public int getAcceptedCount() {
238
239        return m_acceptedCount;
240    }
241
242    /**
243     * Returns the version of the user accepted agreement.<p>
244     *
245     * @return the version of the user accepted agreement
246     */
247    public double getAcceptedVersion() {
248
249        return m_acceptedVersion;
250    }
251
252    /**
253     * Returns the content value of the given path as String.<p>
254     *
255     * @param path the path to get the content value for
256     *
257     * @return the content value of the given path as String
258     */
259    public String getConfigurationContentStringValue(String path) {
260
261        if (getConfigurationContent() != null) {
262            return getConfigurationContent().getStringValue(getCms(), path, getLocale());
263        }
264        return "";
265    }
266
267    /**
268     * Returns the absolute path in the OpenCms VFS to the user agreement configuration file.<p>
269     *
270     * @return the absolute path in the OpenCms VFS to the user agreement configuration file
271     */
272    public String getConfigurationVfsPath() {
273
274        return VFS_PATH_CONFIGFOLDER + getLocale().toString() + "/configuration.html";
275    }
276
277    /**
278     * Returns the originally requested workplace resource path parameter.<p>
279     *
280     * @return the originally requested workplace resource path parameter
281     */
282    public String getParamWpres() {
283
284        if (CmsStringUtil.isEmptyOrWhitespaceOnly(m_paramWpres) || "null".equals(m_paramWpres)) {
285            return CmsVaadinUtils.getWorkplaceLink();
286        }
287        return m_paramWpres;
288    }
289
290    /**
291     * Returns the required version of the user accepted agreement.<p>
292     *
293     * @return the required version of the user accepted agreement
294     */
295    public double getRequiredVersion() {
296
297        if (m_requiredVersion == 0) {
298            String versionStr = getConfigurationContentStringValue(NODE_VERSION);
299            try {
300                m_requiredVersion = Double.parseDouble(versionStr);
301            } catch (Exception e) {
302                // the version number is not in the correct format
303                LOG.error(
304                    Messages.get().getBundle().key(
305                        Messages.LOG_USERAGREEMENT_WRONG_VERSION_2,
306                        versionStr,
307                        getConfigurationContent().getFile().getRootPath()));
308            }
309        }
310        return m_requiredVersion;
311    }
312
313    /**
314     * Returns if the user agreement page should be shown for the current user.<p>
315     *
316     * @return <code>true</code> if the user agreement page should be shown for the current user, otherwise <code>false</code>
317     */
318    public boolean isShowUserAgreement() {
319
320        if (!getSettings().isUserAgreementAccepted() && (getConfigurationContent() != null)) {
321            CmsXmlContent content = getConfigurationContent();
322            boolean enabled = false;
323            try {
324                // first read the property that contains the information if the agreement is enabled at all
325                enabled = Boolean.valueOf(
326                    getCms().readPropertyObject(
327                        content.getFile(),
328                        CmsPropertyDefinition.PROPERTY_LOGIN_FORM,
329                        false).getValue()).booleanValue();
330                if (enabled) {
331                    // user agreement is enabled, now check version and accepted count
332                    if (content.hasLocale(getLocale())) {
333                        if (getAcceptedVersion() < getRequiredVersion()) {
334                            // new user agreement version that has to be shown
335                            return true;
336                        } else {
337                            // check how often the user accepted the user agreement
338                            String countStr = content.getStringValue(getCms(), NODE_AGREE_COUNT, getLocale());
339                            int count = Integer.parseInt(countStr);
340                            if ((count == -1) || (getAcceptedCount() < count)) {
341                                // user still has to accept the user agreement
342                                return true;
343                            }
344                        }
345                    }
346                }
347            } catch (Exception e) {
348                // error when trying to determine if user agreement should be shown
349                LOG.error(
350                    Messages.get().getBundle().key(
351                        Messages.LOG_USERAGREEMENT_SHOW_1,
352                        getConfigurationContent().getFile().getRootPath()),
353                    e);
354            }
355
356        }
357        // store information that nothing has to be accepted in users session for better performance
358        if (!getSettings().isUserAgreementAccepted()) {
359            getSettings().setUserAgreementAccepted(true);
360        }
361        return false;
362    }
363
364    /**
365     * Sets the number of times the user accepted the agreement.<p>
366     *
367     * @param acceptedCount the number of times the user accepted the agreement
368     */
369    public void setAcceptedCount(int acceptedCount) {
370
371        m_acceptedCount = acceptedCount;
372    }
373
374    /**
375     * Sets the version of the user accepted agreement.<p>
376     *
377     * @param acceptedVersion the version of the user accepted agreement
378     */
379    public void setAcceptedVersion(double acceptedVersion) {
380
381        m_acceptedVersion = acceptedVersion;
382    }
383
384    /**
385     * Sets the originally requested workplace resource path parameter.<p>
386     *
387     * @param paramWpres the originally requested workplace resource path parameter
388     */
389    public void setParamWpres(String paramWpres) {
390
391        m_paramWpres = paramWpres;
392    }
393
394    /**
395     * Sets the required version of the user accepted agreement.<p>
396     *
397     * @param requiredVersion the required version of the user accepted agreement
398     */
399    public void setRequiredVersion(double requiredVersion) {
400
401        m_requiredVersion = requiredVersion;
402    }
403
404    /**
405     * The standard "OK" and "Cancel" buttons are overridden to show other labels.<p>
406     *
407     * See also {@link CmsDialog#dialogButtonsHtml(StringBuffer, int, String)}
408     *
409     * @param result a string buffer where the rendered HTML gets appended to
410     * @param button a integer key to identify the button
411     * @param attribute an optional string with possible tag attributes, or null
412     */
413    @Override
414    protected void dialogButtonsHtml(StringBuffer result, int button, String attribute) {
415
416        attribute = appendDelimiter(attribute);
417
418        switch (button) {
419            case BUTTON_OK:
420                result.append("<input name=\"ok\" value=\"");
421                result.append(getConfigurationContentStringValue(NODE_BUTTON_ACCEPT));
422                result.append("\"");
423                if (attribute.toLowerCase().indexOf("onclick") == -1) {
424                    result.append(" type=\"submit\"");
425                } else {
426                    result.append(" type=\"button\"");
427                }
428                result.append(" class=\"dialogbutton\"");
429                result.append(attribute);
430                result.append(">\n");
431                break;
432            case BUTTON_CANCEL:
433                result.append("<input name=\"cancel\" type=\"button\" value=\"");
434                result.append(getConfigurationContentStringValue(NODE_BUTTON_DECLINE));
435                result.append("\"");
436                if (attribute.toLowerCase().indexOf("onclick") == -1) {
437                    result.append(" onclick=\"submitAction('" + DIALOG_CANCEL + "', form);\"");
438                }
439                result.append(" class=\"dialogbutton\"");
440                result.append(attribute);
441                result.append(">\n");
442                break;
443            default:
444                // other buttons are not overridden, call super implementation
445                super.dialogButtonsHtml(result, button, attribute);
446        }
447    }
448
449    /**
450     * Returns the user agreement configuration content.<p>
451     *
452     * @return the user agreement configuration content
453     */
454    protected CmsXmlContent getConfigurationContent() {
455
456        if (m_configurationContent == null) {
457            String configFileName = getConfigurationVfsPath();
458            if (getCms().existsResource(configFileName)) {
459                // configuration file found, check VFS cache for unmarshalled content
460                CmsVfsMemoryObjectCache vfsCache = CmsVfsMemoryObjectCache.getVfsMemoryObjectCache();
461                m_configurationContent = (CmsXmlContent)vfsCache.getCachedObject(getCms(), configFileName);
462                if (m_configurationContent == null) {
463                    // content not found in cache, read the file and unmarshal it
464                    try {
465                        CmsFile configFile = getCms().readFile(configFileName);
466                        CmsXmlContent content = CmsXmlContentFactory.unmarshal(getCms(), configFile);
467                        // put the result content in the cache
468                        vfsCache.putCachedObject(getCms(), configFileName, content);
469                        m_configurationContent = content;
470                    } catch (CmsException e) {
471                        // should never happen, because we checked the resource existence before
472                    }
473                }
474            }
475        }
476        return m_configurationContent;
477    }
478
479    /**
480     * Initializes the 'accepted' data from the current user.<p>
481     * Returns the absolute path in the OpenCms VFS to the user agreement configuration file.<p>
482     */
483    protected void initAcceptData() {
484
485        // read the current users agreement values
486        CmsUser user = getCms().getRequestContext().getCurrentUser();
487        String result = (String)user.getAdditionalInfo(CmsUserSettings.LOGIN_USERAGREEMENT_ACCEPTED);
488        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(result)) {
489            // read JSON data structure that is stored in the user additional info
490            try {
491                JSONObject jsonData = new JSONObject(result);
492                m_acceptedVersion = jsonData.getDouble(KEY_ACCEPTED_VERSION);
493                m_acceptedCount = jsonData.getInt(KEY_ACCEPTED_COUNT);
494            } catch (JSONException e) {
495                LOG.error(e.getLocalizedMessage(), e);
496            }
497        }
498    }
499
500    /**
501     * @see org.opencms.workplace.CmsWorkplace#initWorkplaceRequestValues(org.opencms.workplace.CmsWorkplaceSettings, javax.servlet.http.HttpServletRequest)
502     */
503    @Override
504    protected void initWorkplaceRequestValues(CmsWorkplaceSettings settings, HttpServletRequest request) {
505
506        // fill the parameter values in the get/set methods
507        fillParamValues(request);
508
509        initAcceptData();
510
511        // set the dialog type
512        setParamDialogtype(DIALOG_TYPE);
513        // set the action for the JSP switch
514        if (DIALOG_TYPE.equals(getParamAction())) {
515            setAction(ACTION_ACCEPT);
516        } else if (DIALOG_CANCEL.equals(getParamAction())) {
517            setAction(ACTION_CANCEL);
518        } else {
519            setAction(ACTION_DEFAULT);
520            // build the title for the user agreement dialog
521            setParamTitle(getConfigurationContentStringValue(NODE_DIALOG_TITLE));
522        }
523    }
524
525}