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