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.editors.directedit;
029
030import org.opencms.ade.configuration.CmsADEConfigData;
031import org.opencms.ade.configuration.CmsResourceTypeConfig;
032import org.opencms.ade.contenteditor.shared.CmsEditorConstants;
033import org.opencms.file.CmsResource;
034import org.opencms.file.CmsResourceFilter;
035import org.opencms.file.CmsVfsResourceNotFoundException;
036import org.opencms.file.types.CmsResourceTypeBinary;
037import org.opencms.file.types.CmsResourceTypeImage;
038import org.opencms.file.types.CmsResourceTypePlain;
039import org.opencms.file.types.CmsResourceTypeXmlContent;
040import org.opencms.file.types.CmsResourceTypeXmlPage;
041import org.opencms.file.types.I_CmsResourceType;
042import org.opencms.gwt.shared.CmsGwtConstants;
043import org.opencms.gwt.shared.I_CmsAutoBeanFactory;
044import org.opencms.gwt.shared.I_CmsContentLoadCollectorInfo;
045import org.opencms.gwt.shared.I_CmsEditableDataExtensions;
046import org.opencms.i18n.CmsEncoder;
047import org.opencms.i18n.CmsMessages;
048import org.opencms.json.JSONException;
049import org.opencms.json.JSONObject;
050import org.opencms.jsp.util.CmsJspStandardContextBean;
051import org.opencms.lock.CmsLock;
052import org.opencms.main.CmsException;
053import org.opencms.main.CmsLog;
054import org.opencms.main.OpenCms;
055import org.opencms.security.CmsPermissionSet;
056import org.opencms.security.CmsPermissionViolationException;
057import org.opencms.util.CmsStringUtil;
058import org.opencms.util.CmsUUID;
059import org.opencms.workplace.editors.Messages;
060import org.opencms.xml.containerpage.CmsContainerElementBean;
061
062import java.util.Arrays;
063import java.util.List;
064import java.util.Locale;
065import java.util.Random;
066
067import javax.servlet.jsp.JspException;
068import javax.servlet.jsp.PageContext;
069
070import org.apache.commons.logging.Log;
071
072import com.google.web.bindery.autobean.shared.AutoBean;
073import com.google.web.bindery.autobean.shared.AutoBeanCodex;
074import com.google.web.bindery.autobean.vm.AutoBeanFactorySource;
075
076/**
077 * Provider for the OpenCms AdvancedDirectEdit.<p>
078 *
079 * Since OpenCms version 8.0.0.<p>
080 *
081 * This provider DOES NOT support {@link CmsDirectEditMode#MANUAL} mode.<p>
082 *
083 * @since 8.0.0
084 */
085public class CmsAdvancedDirectEditProvider extends A_CmsDirectEditProvider {
086
087    /**
088     * Direct edit permissions according to the sitemap configuration.
089     */
090    public static enum SitemapDirectEditPermissions {
091
092        /** Everything allowed. */
093        all(true, true, true),
094
095        /** Not found in sitemap config. */
096        editAndCreate(true, true, false),
097
098        /** Can edit, but not add. */
099        editOnly(false, true, true),
100
101        /** Nothing allowed. */
102        none(false, false, false);
103
104        /** True if creating elements is allowed. */
105        private boolean m_create;
106
107        /** True if editing elements is allowed. */
108        private boolean m_edit;
109
110        /** Allow favoriting. */
111        private boolean m_favorite;
112
113        /**
114         * Private constructor.
115         *
116         * @param create true if creation is allowed
117         * @param edit true if editing is allowed
118         * @param favorite true if favoriting is allowed
119         */
120        SitemapDirectEditPermissions(boolean create, boolean edit, boolean favorite) {
121
122            m_create = create;
123            m_edit = edit;
124            m_favorite = favorite;
125        }
126
127        /**
128         * Returns true if element creation is allowed.
129         *
130         * @return true if element creation is allowed
131         */
132        public boolean canCreate() {
133
134            return m_create;
135        }
136
137        /**
138         * Returns true if editing elements is allowed.
139         *
140         * @return true if editing elements is allowed
141         */
142        public boolean canEdit() {
143
144            return m_edit;
145        }
146
147        /**
148         * Return true if favoriting is allowed.
149         *
150         * @return true if favoriting is allowed
151         */
152        public boolean canFavorite() {
153
154            return m_favorite;
155        }
156    }
157
158    /** The log object for this class. */
159    private static final Log LOG = CmsLog.getLog(CmsAdvancedDirectEditProvider.class);
160
161    /** Indicates the permissions for the last element the was opened. */
162    protected int m_lastPermissionMode;
163
164    /** True if the elements should be assigned randomly generated ids. */
165    protected boolean m_useIds;
166
167    /** Factory for the editable data extended attributes. */
168    I_CmsAutoBeanFactory m_editableDataExtensionsFactory = AutoBeanFactorySource.create(I_CmsAutoBeanFactory.class);
169
170    /** The random number generator used for element ids. */
171    private Random m_random = new Random();
172
173    /**
174     * Returns the end HTML for a disabled direct edit button.<p>
175     *
176     * @return the end HTML for a disabled direct edit button
177     */
178    public String endDirectEditDisabled() {
179
180        return "";
181    }
182
183    /**
184     * Returns the end HTML for an enabled direct edit button.<p>
185     *
186     * @return the end HTML for an enabled direct edit button
187     */
188    public String endDirectEditEnabled() {
189
190        String tag = CmsGwtConstants.CLASS_EDITABLE_END;
191
192        return "<" + tag + " class=\"" + CmsGwtConstants.CLASS_EDITABLE_END + "\"></" + tag + ">\n";
193    }
194
195    /**
196     * Generates a random element id.<p>
197     *
198     * @return a random  element id
199     */
200    public synchronized String getRandomId() {
201
202        return "editable_" + Math.abs(m_random.nextLong());
203    }
204
205    /**
206     * @see org.opencms.workplace.editors.directedit.A_CmsDirectEditProvider#getResourceInfo(java.lang.String)
207     *
208     * Similar to the method in the superclass, but removes the write permission check, as this is handled differently.
209     */
210    @Override
211    public CmsDirectEditResourceInfo getResourceInfo(CmsDirectEditParams params, String resourceName) {
212
213        try {
214            // first check some simple preconditions for direct edit
215            if (m_cms.getRequestContext().getCurrentProject().isOnlineProject()) {
216                // don't show direct edit button in online project
217                return CmsDirectEditResourceInfo.INACTIVE;
218            }
219            if (CmsResource.isTemporaryFileName(resourceName)) {
220                // don't show direct edit button on a temporary file
221                return CmsDirectEditResourceInfo.INACTIVE;
222            }
223            if (!m_cms.isInsideCurrentProject(resourceName)) {
224                // don't show direct edit button on files not belonging to the current project
225                return CmsDirectEditResourceInfo.INACTIVE;
226            }
227            // read the target resource
228            CmsResource resource = m_cms.readResource(resourceName, CmsResourceFilter.ALL);
229            if (!OpenCms.getResourceManager().getResourceType(resource.getTypeId()).isDirectEditable()
230                && !resource.isFolder()) {
231                if (CmsStringUtil.isEmptyOrWhitespaceOnly(params.getUploadFolder())) {
232                    return CmsDirectEditResourceInfo.INACTIVE;
233                }
234            }
235            // check the resource lock
236            CmsLock lock = m_cms.getLock(resource);
237            boolean locked = !(lock.isUnlocked()
238                || lock.isOwnedInProjectBy(
239                    m_cms.getRequestContext().getCurrentUser(),
240                    m_cms.getRequestContext().getCurrentProject()));
241            // check the users permissions on the resource
242            // only if write permissions are granted the resource may be direct editable
243            if (locked) {
244                // a locked resource must be shown as "disabled"
245                return new CmsDirectEditResourceInfo(CmsDirectEditPermissions.DISABLED, resource, lock);
246            }
247            // if we have write permission and the resource is not locked then direct edit is enabled
248            return new CmsDirectEditResourceInfo(CmsDirectEditPermissions.ENABLED, resource, lock);
249
250        } catch (Exception e) {
251            // all exceptions: don't mix up the result HTML, always return INACTIVE mode
252            if (LOG.isWarnEnabled()) {
253                LOG.warn(
254                    org.opencms.workplace.editors.Messages.get().getBundle().key(
255                        org.opencms.workplace.editors.Messages.LOG_CALC_EDIT_MODE_FAILED_1,
256                        resourceName),
257                    e);
258            }
259        }
260        // otherwise the resource is not direct editable
261        return CmsDirectEditResourceInfo.INACTIVE;
262    }
263
264    /**
265     * @see org.opencms.workplace.editors.directedit.I_CmsDirectEditProvider#insertDirectEditEnd(javax.servlet.jsp.PageContext)
266     */
267    public void insertDirectEditEnd(PageContext context) throws JspException {
268
269        String content;
270        switch (m_lastPermissionMode) {
271
272            case 1: // disabled
273                //                content = endDirectEditDisabled();
274                //                break;
275            case 2: // enabled
276                content = endDirectEditEnabled();
277                break;
278            default: // inactive or undefined
279                content = null;
280        }
281        m_lastPermissionMode = 0;
282        print(context, content);
283    }
284
285    /**
286     * @see org.opencms.workplace.editors.directedit.I_CmsDirectEditProvider#insertDirectEditIncludes(javax.servlet.jsp.PageContext, org.opencms.workplace.editors.directedit.CmsDirectEditParams)
287     */
288    @SuppressWarnings("unused")
289    public void insertDirectEditIncludes(PageContext context, CmsDirectEditParams params) throws JspException {
290
291        // For Advanced Direct Edit all necessary js and css-code is included by the enableADE tag. Further includes in the head are not needed.
292
293    }
294
295    /**
296     * @see org.opencms.workplace.editors.directedit.A_CmsDirectEditProvider#insertDirectEditListMetadata(javax.servlet.jsp.PageContext, org.opencms.gwt.shared.I_CmsContentLoadCollectorInfo)
297     */
298    @Override
299    public void insertDirectEditListMetadata(PageContext context, I_CmsContentLoadCollectorInfo info)
300    throws JspException {
301
302        if (m_cms.getRequestContext().getCurrentProject().isOnlineProject()) {
303            // the metadata is only needed for editing
304            return;
305        }
306        I_CmsAutoBeanFactory collectorInfoFactory = AutoBeanFactorySource.create(I_CmsAutoBeanFactory.class);
307        AutoBean<I_CmsContentLoadCollectorInfo> collectorInfoAutoBean = collectorInfoFactory.wrapCollectorInfo(info);
308        String serializedCollectorInfo = AutoBeanCodex.encode(collectorInfoAutoBean).getPayload();
309
310        String tag = CmsGwtConstants.CLASS_COLLECTOR_INFO;
311        String marker = "<"
312            + tag
313            + " class='"
314            + CmsGwtConstants.CLASS_COLLECTOR_INFO
315            + "' style='display: none !important;' "
316            + CmsGwtConstants.ATTR_DATA_COLLECTOR
317            + "='"
318            + CmsEncoder.escapeXml(serializedCollectorInfo)
319            + "'></"
320            + tag
321            + ">";
322        print(context, marker);
323    }
324
325    /**
326     * @see org.opencms.workplace.editors.directedit.I_CmsDirectEditProvider#insertDirectEditStart(javax.servlet.jsp.PageContext, org.opencms.workplace.editors.directedit.CmsDirectEditParams)
327     */
328    public boolean insertDirectEditStart(PageContext context, CmsDirectEditParams params) throws JspException {
329
330        String content;
331        // check the direct edit permissions of the current user
332        CmsDirectEditResourceInfo resourceInfo = getResourceInfo(params, params.getResourceName());
333
334        // check the permission mode
335        m_lastPermissionMode = resourceInfo.getPermissions().getPermission();
336        switch (m_lastPermissionMode) {
337            case 1: // disabled
338                //                content = startDirectEditDisabled(params, resourceInfo);
339                //                break;
340            case 2: // enabled
341                try {
342                    CmsJspStandardContextBean contextBean = CmsJspStandardContextBean.getInstance(context.getRequest());
343                    CmsContainerElementBean element = contextBean.getElement();
344                    if ((element != null) && element.getId().equals(resourceInfo.getResource().getStructureId())) {
345                        params.m_element = element.editorHash();
346                        params.setContainerElement(element);
347                    }
348                    content = startDirectEditEnabled(params, resourceInfo);
349                } catch (JSONException e) {
350                    throw new JspException(e);
351                }
352                break;
353            default: // inactive or undefined
354                content = null;
355        }
356        print(context, content);
357        return content != null;
358    }
359
360    /**
361     * Returns <code>false</code> because the default provider does not support manual button placement.<p>
362     *
363     * @see org.opencms.workplace.editors.directedit.I_CmsDirectEditProvider#isManual(org.opencms.workplace.editors.directedit.CmsDirectEditMode)
364     */
365    @Override
366    public boolean isManual(CmsDirectEditMode mode) {
367
368        return false;
369    }
370
371    /**
372     * @see org.opencms.workplace.editors.directedit.I_CmsDirectEditProvider#newInstance()
373     */
374    public I_CmsDirectEditProvider newInstance() {
375
376        CmsAdvancedDirectEditProvider result = new CmsAdvancedDirectEditProvider();
377        result.m_configurationParameters = m_configurationParameters;
378        return result;
379    }
380
381    /**
382     * Returns the start HTML for a disabled direct edit button.<p>
383     *
384     * @param params the direct edit parameters
385     * @param resourceInfo contains information about the resource to edit
386     *
387     * @return the start HTML for a disabled direct edit button
388     */
389    public String startDirectEditDisabled(CmsDirectEditParams params, CmsDirectEditResourceInfo resourceInfo) {
390
391        StringBuffer result = new StringBuffer(256);
392
393        result.append("<!-- EDIT BLOCK START (DISABLED): ");
394        result.append(params.m_resourceName);
395        result.append(" [");
396        result.append(resourceInfo.getResource().getState());
397        result.append("] ");
398        if (!resourceInfo.getLock().isUnlocked()) {
399            result.append(" locked ");
400            result.append(resourceInfo.getLock().getProject().getName());
401        }
402        result.append(" -->\n");
403        return result.toString();
404    }
405
406    /**
407     * Returns the start HTML for an enabled direct edit button.<p>
408     *
409     * @param params the direct edit parameters
410     * @param resourceInfo contains information about the resource to edit
411     *
412     * @return the start HTML for an enabled direct edit button
413     * @throws JSONException if a JSON handling error occurs
414     */
415    public String startDirectEditEnabled(CmsDirectEditParams params, CmsDirectEditResourceInfo resourceInfo)
416    throws JSONException {
417
418        String editLocale = m_cms.getRequestContext().getLocale().toString();
419        String editId = getNextDirectEditId();
420        String editNewLink = CmsEncoder.encode(params.getLinkForNew());
421        // putting together all needed data
422        JSONObject editableData = new JSONObject();
423        CmsResource resource = resourceInfo.getResource();
424        boolean writable = false;
425        String uri = m_cms.getRequestContext().getUri();
426        uri = m_cms.getRequestContext().addSiteRoot(uri);
427        CmsADEConfigData configData = OpenCms.getADEManager().lookupConfigurationWithCache(m_cms, uri);
428        if (resource != null) {
429            try {
430                writable = m_cms.hasPermissions(
431                    resource,
432                    CmsPermissionSet.ACCESS_WRITE,
433                    false,
434                    CmsResourceFilter.IGNORE_EXPIRATION);
435            } catch (CmsException e) {
436                LOG.error(e.getLocalizedMessage(), e);
437            }
438        }
439        editableData.put("editId", editId);
440        CmsContainerElementBean containerElement = params.getContainerElement();
441        if (containerElement != null) {
442            editableData.put(CmsGwtConstants.ATTR_ELEMENT_ID, containerElement.editorHash());
443        }
444
445        editableData.put("structureId", resourceInfo.getResource().getStructureId());
446        editableData.put("sitePath", params.getResourceName());
447        editableData.put("elementlanguage", editLocale);
448        editableData.put("elementname", params.getElement());
449        editableData.put("newlink", editNewLink);
450        editableData.put("hasResource", resource != null);
451        editableData.put("newtitle", m_messages.key(Messages.GUI_EDITOR_TITLE_NEW_0));
452        editableData.put(
453            "unreleaseOrExpired",
454            !resourceInfo.getResource().isReleasedAndNotExpired(System.currentTimeMillis()));
455        if (params.getId() != null) {
456            editableData.put(CmsEditorConstants.ATTR_CONTEXT_ID, params.getId().toString());
457        }
458        editableData.put(CmsEditorConstants.ATTR_POST_CREATE_HANDLER, params.getPostCreateHandler());
459        CmsUUID viewId = CmsUUID.getNullUUID();
460        boolean hasEditHandler = false;
461        String typeName = null;
462        if ((resourceInfo.getResource() != null) && resourceInfo.getResource().isFile()) {
463            I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(resourceInfo.getResource());
464            if (type instanceof CmsResourceTypeXmlContent) {
465                hasEditHandler = ((CmsResourceTypeXmlContent)type).getEditHandler(m_cms) != null;
466            }
467            typeName = OpenCms.getResourceManager().getResourceType(resourceInfo.getResource()).getTypeName();
468            CmsResourceTypeConfig typeConfig = configData.getResourceType(typeName);
469            if (typeConfig != null) {
470                viewId = typeConfig.getElementView();
471            }
472        } else {
473            String linkForNew = params.getLinkForNew();
474            if (linkForNew != null) {
475                String[] components = linkForNew.split("\\|");
476                typeName = components[components.length - 1];
477                CmsResourceTypeConfig typeConfig = configData.getResourceType(typeName);
478                if (typeConfig != null) {
479                    viewId = typeConfig.getElementView();
480                }
481            }
482        }
483        SitemapDirectEditPermissions sitemapConfigPermissions = configData.getDirectEditPermissions(typeName);
484        boolean hasNew = false;
485        editableData.put(
486            "hasEdit",
487            params.getButtonSelection().isShowEdit()
488                && (CmsResourceTypeXmlContent.isXmlContent(resource) || CmsResourceTypeXmlPage.isXmlPage(resource)));
489        editableData.put(
490            "hasDelete",
491            params.getButtonSelection().isShowDelete() && writable && sitemapConfigPermissions.canEdit());
492        // hasNew = params.getButtonSelection().isShowNew() && writable && sitemapConfigPermissions.canEdit();
493
494        editableData.put(CmsEditorConstants.ATTR_ELEMENT_VIEW, viewId);
495        editableData.put("hasEditHandler", hasEditHandler);
496        boolean favorites = sitemapConfigPermissions.canFavorite();
497        Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(m_cms);
498        CmsMessages messages = Messages.get().getBundle(locale);
499        if ((m_lastPermissionMode == 1) || !writable || (!sitemapConfigPermissions.canEdit())) {
500            String noEditReason = null;
501            if (sitemapConfigPermissions.canCreate()) {
502                noEditReason = messages.key(Messages.GUI_DIRECTEDIT_CAN_ONLY_BE_CREATED_0);// "Can only be created but not edited";
503            } else {
504                noEditReason = messages.key(Messages.GUI_DIRECTEDIT_CANNOT_BE_CREATED_OR_EDITED_0); // "Cannot be created or edited";
505            }
506            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(noEditReason)) {
507                editableData.put("noEditReason", noEditReason);
508            }
509        }
510        editableData.put(CmsGwtConstants.ATTR_FAVORITE, Boolean.valueOf(favorites));
511        AutoBean<I_CmsEditableDataExtensions> extensions = m_editableDataExtensionsFactory.createExtensions();
512        boolean isUploadType = false;
513        List<String> uploadTypes = Arrays.asList(
514            CmsResourceTypeBinary.getStaticTypeName(),
515            CmsResourceTypeImage.getStaticTypeName(),
516            CmsResourceTypePlain.getStaticTypeName());
517        if ((resource != null) && !resource.isFolder()) {
518            for (String type : uploadTypes) {
519                if (OpenCms.getResourceManager().matchResourceType(type, resource.getTypeId())) {
520                    isUploadType = true;
521                    break;
522                }
523            }
524        } else {
525            String newLink = params.getLinkForNew();
526            if (newLink != null) {
527                int pipePos = newLink.lastIndexOf('|');
528                String typePart = newLink.substring(pipePos + 1);
529                isUploadType = uploadTypes.contains(typePart);
530            }
531        }
532        String uploadFolder = params.getUploadFolder();
533        hasNew = params.getButtonSelection().isShowNew();
534        if (isUploadType) {
535            if (!CmsStringUtil.isEmptyOrWhitespaceOnly(uploadFolder) && !"none".equals(uploadFolder)) {
536                CmsResource uploadFolderResource = null;
537                try {
538                    uploadFolderResource = m_cms.readResource(uploadFolder, CmsResourceFilter.IGNORE_EXPIRATION);
539                    hasNew &= m_cms.hasPermissions(
540                        uploadFolderResource,
541                        CmsPermissionSet.ACCESS_WRITE,
542                        false,
543                        CmsResourceFilter.IGNORE_EXPIRATION);
544                } catch (CmsVfsResourceNotFoundException e) {
545                    LOG.debug(e.getLocalizedMessage(), e);
546                } catch (CmsPermissionViolationException e) {
547                    LOG.debug(
548                        "hasNew = false for upload folder " + uploadFolder + ", reason: " + e.getLocalizedMessage(),
549                        e);
550                    hasNew = false;
551                } catch (CmsException e) {
552                    LOG.error(e.getLocalizedMessage(), e);
553                    hasNew = false;
554                }
555            } else {
556                hasNew = false;
557            }
558        } else {
559            hasNew &= sitemapConfigPermissions.canCreate();
560        }
561        extensions.as().setUploadEnabled(isUploadType && hasUploadSupport() && (params.getUploadFolder() != null));
562        extensions.as().setUploadFolder(params.getUploadFolder());
563        editableData.put(CmsGwtConstants.ATTR_EXTENSIONS, AutoBeanCodex.encode(extensions).getPayload());
564        editableData.put("hasNew", hasNew);
565
566        StringBuffer result = new StringBuffer(512);
567        if (m_useIds) {
568            String tag = CmsGwtConstants.CLASS_EDITABLE;
569            result.append(
570                "<"
571                    + tag
572                    + " id=\"" // use custom tag
573                    + getRandomId()
574                    + "\" class='"
575                    + CmsGwtConstants.CLASS_EDITABLE
576                    + "' "
577                    + CmsGwtConstants.ATTR_DATA_EDITABLE
578                    + "='").append(editableData.toString()).append("'></" + tag + ">\n");
579        } else {
580            String tag = CmsGwtConstants.CLASS_EDITABLE;
581            result.append(
582                "<"
583                    + tag
584                    + " class='"
585                    + CmsGwtConstants.CLASS_EDITABLE
586                    + "' "
587                    + CmsGwtConstants.ATTR_DATA_EDITABLE
588                    + "='").append(editableData.toString()).append("'></" + tag + ">\n");
589        }
590        return result.toString();
591    }
592
593    /**
594     * Checks if upload for binaries are supported by this provider.
595     *
596     * @return true if binary upload is supported
597     */
598    protected boolean hasUploadSupport() {
599
600        return true;
601    }
602
603}