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