001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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 GmbH & Co. KG, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://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.commons;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsProject;
032import org.opencms.file.CmsResource;
033import org.opencms.file.CmsResource.CmsResourceUndoMode;
034import org.opencms.file.CmsResourceFilter;
035import org.opencms.jsp.CmsJspActionElement;
036import org.opencms.main.CmsException;
037import org.opencms.main.CmsLog;
038import org.opencms.security.CmsPermissionSet;
039import org.opencms.util.CmsUUID;
040import org.opencms.workplace.CmsMultiDialog;
041import org.opencms.workplace.CmsWorkplaceSettings;
042
043import java.util.ArrayList;
044import java.util.Iterator;
045import java.util.List;
046
047import org.apache.commons.logging.Log;
048
049import jakarta.servlet.http.HttpServletRequest;
050import jakarta.servlet.http.HttpServletResponse;
051import jakarta.servlet.jsp.JspException;
052import jakarta.servlet.jsp.PageContext;
053
054/**
055 * Provides methods for the undo changes on a resource dialog.<p>
056 *
057 * The following files use this class:
058 * <ul>
059 * <li>/commons/undochanges.jsp
060 * </ul>
061 * <p>
062 *
063 * @since 6.0.0
064 */
065public class CmsUndoChanges extends CmsMultiDialog {
066
067    /** Value for the action: undo changes. */
068    public static final int ACTION_UNDOCHANGES = 100;
069
070    /** Value for the action: check for siblings and warn in case they exist. */
071    public static final int ACTION_CHECKSIBLINGS = 101;
072
073    /** Action string constant for the check siblings dialog. */
074    public static final String DIALOG_CHECKSIBLINGS = "checksiblings";
075
076    /** The dialog type. */
077    public static final String DIALOG_TYPE = "undochanges";
078
079    /** Request parameter name for the recursive flag.<p> */
080    public static final String PARAM_RECURSIVE = "recursive";
081
082    /** Request parameter name for the move flag.<p> */
083    public static final String PARAM_MOVE = "move";
084
085    /** The log object for this class. */
086    private static final Log LOG = CmsLog.getLog(CmsUndoChanges.class);
087
088    /** The single current resource. */
089    private CmsResource m_currentResource;
090
091    /** The undo move operation flag parameter value. */
092    private String m_paramMove;
093
094    /** The undo recursively flag parameter value. */
095    private String m_paramRecursive;
096
097    /**
098     * Public constructor with JSP action element.<p>
099     *
100     * @param jsp an initialized JSP action element
101     */
102    public CmsUndoChanges(CmsJspActionElement jsp) {
103
104        super(jsp);
105    }
106
107    /**
108     * Public constructor with JSP variables.<p>
109     *
110     * @param context the JSP page context
111     * @param req the JSP request
112     * @param res the JSP response
113     */
114    public CmsUndoChanges(PageContext context, HttpServletRequest req, HttpServletResponse res) {
115
116        this(new CmsJspActionElement(context, req, res));
117    }
118
119    /**
120     * Returns the original path of given resource, that is the online path for the resource.
121     * If it differs from the offline path, the resource has been moved.<p>
122     *
123     * @param cms the cms context
124     * @param resourceName a site relative resource name
125     *
126     * @return the online path, or <code>null</code> if resource has not been published
127     */
128    public static String resourceOriginalPath(CmsObject cms, String resourceName) {
129
130        CmsProject proj = cms.getRequestContext().getCurrentProject();
131        try {
132            CmsResource resource = cms.readResource(resourceName, CmsResourceFilter.ALL);
133            String result = cms.getSitePath(resource);
134            cms.getRequestContext().setCurrentProject(cms.readProject(CmsProject.ONLINE_PROJECT_ID));
135            result = cms.getSitePath(cms.readResource(resource.getStructureId()));
136            // remove '/' if needed
137            if (result.charAt(result.length() - 1) == '/') {
138                if (resourceName.charAt(resourceName.length() - 1) != '/') {
139                    result = result.substring(0, result.length() - 1);
140                }
141            }
142            return result;
143        } catch (CmsException e) {
144            return null;
145        } finally {
146            cms.getRequestContext().setCurrentProject(proj);
147        }
148    }
149
150    /**
151     * Performs the check for siblings action and returns false in case of existence.<p>
152     *
153     * @return true if siblings are found.
154     */
155    public boolean actionCheckSiblings() {
156
157        List<String> resourceList = getResourceList();
158        String resourcePath;
159        Iterator<String> itResourcePaths = resourceList.iterator();
160        boolean foundSibling = false;
161        while (itResourcePaths.hasNext()) {
162            resourcePath = itResourcePaths.next();
163            try {
164                foundSibling = recursiveCheckSiblings(resourcePath);
165                if (foundSibling) {
166                    break; // shortcut
167                }
168
169            } catch (CmsException e) {
170                LOG.error(
171                    Messages.get().getBundle(getLocale()).key(
172                        Messages.ERR_UNDO_CHANGES_1,
173                        new String[] {resourcePath}));
174            }
175
176        }
177        return foundSibling;
178    }
179
180    /**
181     * Performs the undo changes action, will be called by the JSP page.<p>
182     *
183     * @throws JspException if problems including sub-elements occur
184     */
185    public void actionUndoChanges() throws JspException {
186
187        // save initialized instance of this class in request attribute for included sub-elements
188        getJsp().getRequest().setAttribute(SESSION_WORKPLACE_CLASS, this);
189        try {
190            boolean isFolder = false;
191            String source = getResourceList().get(0);
192            if (!isMultiOperation()) {
193                CmsResource resource = getCms().readResource(source, CmsResourceFilter.ALL);
194                isFolder = resource.isFolder();
195            }
196            // get the folders to refresh
197            List<String> folderList = new ArrayList<String>(1 + getResourceList().size());
198            folderList.add(CmsResource.getParentFolder(source));
199            Iterator<String> it = getResourceList().iterator();
200            while (it.hasNext()) {
201                String res = it.next();
202                String target = resourceOriginalPath(getCms(), res);
203                if ((target != null) && !target.equals(res)) {
204                    CmsResource resource = getCms().readResource(res, CmsResourceFilter.ALL);
205                    if (resource.isFolder()) {
206                        folderList.add(CmsResource.getParentFolder(target));
207                    }
208                }
209            }
210            if (performDialogOperation()) {
211                // if no exception is caused and "true" is returned move operation was successful
212                if (isMultiOperation() || isFolder) {
213                    // set request attribute to reload the explorer tree view
214                    getJsp().getRequest().setAttribute(REQUEST_ATTRIBUTE_RELOADTREE, folderList);
215                }
216                actionCloseDialog();
217            } else {
218                // "false" returned, display "please wait" screen
219                getJsp().include(FILE_DIALOG_SCREEN_WAIT);
220            }
221        } catch (Throwable e) {
222            // error during deletion, show error dialog
223            includeErrorpage(this, e);
224        }
225    }
226
227    /**
228     * Returns the HTML for the undo changes options and detailed output for single resource operations.<p>
229     *
230     * @return the HTML for the undo changes options
231     */
232    public String buildDialogOptions() {
233
234        StringBuffer result = new StringBuffer(256);
235
236        boolean isMoved = isOperationOnMovedResource();
237        if (!isMultiOperation()) {
238            result.append(dialogSpacer());
239            result.append(
240                key(
241                    Messages.GUI_UNDO_LASTMODIFIED_INFO_3,
242                    new Object[] {getFileName(), getLastModifiedDate(), getLastModifiedUser()}));
243            if (isMoved) {
244                result.append(dialogSpacer());
245                result.append(
246                    key(
247                        Messages.GUI_UNDO_MOVE_OPERATION_INFO_2,
248                        new Object[] {getFileName(), resourceOriginalPath(getCms(), getParamResource())}));
249            }
250        }
251        result.append(dialogSpacer());
252        result.append(key(Messages.GUI_UNDO_CONFIRMATION_0));
253        if (isMoved || isOperationOnFolder()) {
254            // show undo move option if both options are available
255            result.append(dialogSpacer());
256            result.append("<input type=\"checkbox\" name=\"");
257            result.append(PARAM_MOVE);
258            result.append("\" value=\"true\" checked='checked'>&nbsp;");
259            if (isMultiOperation()) {
260                result.append(key(Messages.GUI_UNDO_CHANGES_MOVE_MULTI_SUBRESOURCES_0));
261            } else {
262                result.append(key(Messages.GUI_UNDO_CHANGES_MOVE_SUBRESOURCES_0));
263            }
264        } else {
265            if (isMoved) {
266                result.append(dialogSpacer());
267                result.append("<input type=\"hidden\" name=\"");
268                result.append(PARAM_MOVE);
269                result.append("\" value=\"true\">&nbsp;");
270            }
271        }
272        if (isOperationOnFolder()) {
273            // show recursive option if folder(s) is/are selected
274            result.append(dialogSpacer());
275            result.append(dialogBlockStart(key(Messages.GUI_UNDO_CHANGES_RECURSIVE_TITLE_0)));
276            result.append("<input type=\"checkbox\" name=\"");
277            result.append(PARAM_RECURSIVE);
278            result.append("\" value=\"true\">&nbsp;");
279            if (isMultiOperation()) {
280                result.append(key(Messages.GUI_UNDO_CHANGES_RECURSIVE_MULTI_SUBRESOURCES_0));
281            } else {
282                result.append(key(Messages.GUI_UNDO_CHANGES_RECURSIVE_SUBRESOURCES_0));
283            }
284            result.append(dialogBlockEnd());
285        }
286        return result.toString();
287    }
288
289    /**
290     * Returns the undo move operation flag parameter value.<p>
291     *
292     * @return the undo move operation flag parameter value
293     */
294    public String getParamMove() {
295
296        return m_paramMove;
297    }
298
299    /**
300     * Returns the value of the recursive parameter,
301     * or <code>null</code> if this parameter was not provided.<p>
302     *
303     * The recursive parameter on folders decides if all subresources
304     * of the folder should be unchanged, too.<p>
305     *
306     * @return the value of the recursive parameter
307     */
308    public String getParamRecursive() {
309
310        return m_paramRecursive;
311    }
312
313    /**
314     * Sets the undo move operation flag parameter value.<p>
315     *
316     * @param paramMove the undo move operation flag to set
317     */
318    public void setParamMove(String paramMove) {
319
320        m_paramMove = paramMove;
321    }
322
323    /**
324     * Sets the value of the recursive parameter.<p>
325     *
326     * @param value the value to set
327     */
328    public void setParamRecursive(String value) {
329
330        m_paramRecursive = value;
331    }
332
333    /**
334     * Returns the current CmsResource.<p>
335     *
336     * @return the CmsResource
337     */
338    protected CmsResource getCurrentResource() {
339
340        return m_currentResource;
341    }
342
343    /**
344     * Returns the file name without path information of the current resource.<p>
345     *
346     * @return the name of the current resource
347     */
348    protected String getFileName() {
349
350        return CmsResource.getName(getParamResource());
351    }
352
353    /**
354     * Returns the last modified date of the current resource as localized String.<p>
355     *
356     * @return the date of last modification
357     */
358    protected String getLastModifiedDate() {
359
360        long dateLong = getCurrentResource().getDateLastModified();
361        return getMessages().getDateTime(dateLong);
362    }
363
364    /**
365     * Returns the user who made the last changes to the current resource.<p>
366     *
367     * @return the user who changed the resource
368     */
369    protected String getLastModifiedUser() {
370
371        CmsUUID userId = getCurrentResource().getUserLastModified();
372        try {
373            return getCms().readUser(userId).getName();
374        } catch (CmsException e) {
375            return "";
376        }
377    }
378
379    /**
380     * @see org.opencms.workplace.CmsWorkplace#initWorkplaceRequestValues(org.opencms.workplace.CmsWorkplaceSettings, jakarta.servlet.http.HttpServletRequest)
381     */
382    @Override
383    protected void initWorkplaceRequestValues(CmsWorkplaceSettings settings, HttpServletRequest request) {
384
385        // fill the parameter values in the get/set methods
386        fillParamValues(request);
387
388        // check the required permissions to undo changes of the resource
389        if (!checkResourcePermissions(CmsPermissionSet.ACCESS_WRITE, false)) {
390            // no write permissions for the resource, set cancel action to close dialog
391            setParamAction(DIALOG_CANCEL);
392        }
393
394        // set the dialog type
395        setParamDialogtype(DIALOG_TYPE);
396        // set the action for the JSP switch
397        if (DIALOG_TYPE.equals(getParamAction())) {
398            setAction(ACTION_UNDOCHANGES);
399        } else if (DIALOG_CHECKSIBLINGS.equals(getParamAction())) {
400            setAction(ACTION_CHECKSIBLINGS);
401        } else if (DIALOG_WAIT.equals(getParamAction())) {
402            setAction(ACTION_WAIT);
403        } else if (DIALOG_CANCEL.equals(getParamAction())) {
404            setAction(ACTION_CANCEL);
405        } else if (DIALOG_LOCKS_CONFIRMED.equals(getParamAction())) {
406            setAction(ACTION_LOCKS_CONFIRMED);
407        } else {
408            setAction(ACTION_DEFAULT);
409            // build title for undo changes dialog
410            setDialogTitle(Messages.GUI_UNDO_CHANGES_1, Messages.GUI_UNDO_CHANGES_MULTI_2);
411        }
412
413        if (!isMultiOperation()) {
414            // collect resource to display information on single operation dialog
415            try {
416                setCurrentResource(getCms().readResource(getParamResource(), CmsResourceFilter.ALL));
417            } catch (CmsException e) {
418                // should usually never happen
419                if (LOG.isInfoEnabled()) {
420                    LOG.info(e.getLocalizedMessage());
421                }
422            }
423        }
424
425    }
426
427    /**
428     * Checks if the resource operation is an operation on at least one moved resource.<p>
429     *
430     * @return true if the operation an operation on at least one moved resource, otherwise false
431     */
432    protected boolean isOperationOnMovedResource() {
433
434        Iterator<String> i = getResourceList().iterator();
435        while (i.hasNext()) {
436            String resName = i.next();
437            String target = resourceOriginalPath(getCms(), resName);
438            if ((target != null) && !target.equals(resName)) {
439                // found a moved resource
440                return true;
441            }
442        }
443        return false;
444    }
445
446    /**
447     * Performs the undo changes operation on a resource.<p>
448     *
449     * @return true, if the changes on a resource were undone, otherwise false
450     * @throws CmsException if undo changes is not successful
451     */
452    @Override
453    protected boolean performDialogOperation() throws CmsException {
454
455        // check if the current resource is a folder for single operation
456        boolean isFolder = isOperationOnFolder();
457        // on folder undo changes or multi operation display "please wait" screen, not for simple file undo changes
458        if ((isMultiOperation() || isFolder) && !DIALOG_WAIT.equals(getParamAction())) {
459            // return false, this will trigger the "please wait" screen
460            return false;
461        }
462
463        // get the flag if the undo changes is recursive from request parameter
464        boolean undoRecursive = Boolean.valueOf(getParamRecursive()).booleanValue();
465        boolean undoMove = Boolean.valueOf(getParamMove()).booleanValue();
466
467        CmsResourceUndoMode mode = CmsResource.UNDO_CONTENT;
468        if (undoRecursive) {
469            mode = CmsResource.UNDO_CONTENT_RECURSIVE;
470        }
471        if (undoMove) {
472            mode = mode.includeMove();
473        }
474
475        Iterator<String> i = getResourceList().iterator();
476        // iterate the resources to delete
477        while (i.hasNext()) {
478            String resName = i.next();
479            try {
480                // lock resource if autolock is enabled
481                checkLock(resName);
482                // undo changes on the resource
483                getCms().undoChanges(resName, mode);
484            } catch (CmsException e) {
485                if (isMultiOperation()) {
486                    // collect exceptions to create a detailed output
487                    addMultiOperationException(e);
488                } else {
489                    // for single operation, throw the exception immediately
490                    throw e;
491                }
492            }
493        }
494        // check if exceptions occurred
495        checkMultiOperationException(Messages.get(), Messages.ERR_UNDO_CHANGES_MULTI_0);
496
497        return true;
498    }
499
500    /**
501     * Sets the current CmsResource.<p>
502     *
503     * @param res the CmsResource
504     */
505    protected void setCurrentResource(CmsResource res) {
506
507        m_currentResource = res;
508    }
509
510    /**
511     * Depth first recursion for searching if a file has siblings with early termination.<p>
512     *
513     * This avoids to read a whole resource tree (which will be hard for e.g.  /sites/).<p>
514     *
515     * @param path the path to check the siblings in
516     *
517     * @return true if a resource which is a sibling was found.
518     *
519     * @throws CmsException if something goes wrong
520     */
521    private boolean recursiveCheckSiblings(String path) throws CmsException {
522
523        boolean result = false;
524        CmsObject cms = getCms();
525        /*
526         * No trailing slash for the multi dialogs... therefore a read resource first necessary:
527         */
528        CmsResource resource = cms.readResource(path);
529        path = cms.getSitePath(resource);
530        if (CmsResource.isFolder(path)) {
531            // only check subresources in case of recursive option checked.
532            boolean undoRecursive = Boolean.valueOf(getParamRecursive()).booleanValue();
533            if (undoRecursive) {
534                // don't read the whole tree, this is  expensive - most likely we will find a sibling faster step by step:
535                List<CmsResource> subResources = cms.readResources(path, CmsResourceFilter.ALL, false);
536                Iterator<CmsResource> itSubResources = subResources.iterator();
537                while (itSubResources.hasNext()) {
538                    resource = itSubResources.next();
539                    result = recursiveCheckSiblings(cms.getSitePath(resource));
540                    if (result) {
541                        break; // shortcut
542                    }
543                }
544            }
545
546        } else {
547            List<CmsResource> siblings = cms.readSiblings(path, CmsResourceFilter.ALL);
548            result = siblings.size() > 1;
549        }
550        return result;
551    }
552
553}