001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (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, 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.ade.postupload;
029
030import org.opencms.ade.configuration.CmsADEConfigData;
031import org.opencms.ade.postupload.shared.CmsPostUploadDialogBean;
032import org.opencms.ade.postupload.shared.CmsPostUploadDialogPanelBean;
033import org.opencms.ade.postupload.shared.I_CmsDialogConstants;
034import org.opencms.ade.postupload.shared.rpc.I_CmsPostUploadDialogService;
035import org.opencms.file.CmsObject;
036import org.opencms.file.CmsProperty;
037import org.opencms.file.CmsPropertyDefinition;
038import org.opencms.file.CmsResource;
039import org.opencms.file.CmsResourceFilter;
040import org.opencms.file.types.CmsResourceTypeImage;
041import org.opencms.file.types.I_CmsResourceType;
042import org.opencms.flex.CmsFlexController;
043import org.opencms.gwt.CmsGwtService;
044import org.opencms.gwt.CmsPropertyEditorHelper;
045import org.opencms.gwt.CmsRpcException;
046import org.opencms.gwt.CmsVfsService;
047import org.opencms.gwt.shared.CmsListInfoBean;
048import org.opencms.gwt.shared.property.CmsClientProperty;
049import org.opencms.gwt.shared.property.CmsPropertyModification;
050import org.opencms.lock.CmsLockUtil;
051import org.opencms.main.CmsException;
052import org.opencms.main.CmsLog;
053import org.opencms.main.CmsPermalinkResourceHandler;
054import org.opencms.main.OpenCms;
055import org.opencms.ui.util.CmsGalleryFilePreview;
056import org.opencms.util.CmsMacroResolver;
057import org.opencms.util.CmsStringUtil;
058import org.opencms.util.CmsUUID;
059import org.opencms.workplace.explorer.CmsExplorerTypeSettings;
060import org.opencms.xml.content.CmsXmlContentProperty;
061import org.opencms.xml.content.CmsXmlContentPropertyHelper;
062
063import java.util.ArrayList;
064import java.util.HashSet;
065import java.util.LinkedHashMap;
066import java.util.LinkedHashSet;
067import java.util.List;
068import java.util.Locale;
069import java.util.Map;
070import java.util.Objects;
071import java.util.Set;
072import java.util.stream.Collectors;
073
074import javax.servlet.http.HttpServletRequest;
075
076import org.apache.commons.logging.Log;
077
078import com.google.common.collect.Iterables;
079
080/**
081 * The service implementation for the org.opencms.ade.postupload module.<p>
082 */
083public class CmsPostUploadDialogService extends CmsGwtService implements I_CmsPostUploadDialogService {
084
085    /** Logger instance for this class. */
086    private static final Log LOG = CmsLog.getLog(CmsPostUploadDialogService.class);
087
088    /** Serial version id. */
089    private static final long serialVersionUID = 1L;
090
091    /**
092     * Creates a new instance.<p>
093     */
094    public CmsPostUploadDialogService() {
095
096        super();
097    }
098
099    /**
100     * Fetches the dialog data.<p>
101     *
102     * @param request the servlet request
103     *
104     * @return the dialog data
105     * @throws CmsRpcException if something goes wrong
106     */
107    public static CmsPostUploadDialogBean prefetch(HttpServletRequest request) throws CmsRpcException {
108
109        CmsPostUploadDialogService srv = new CmsPostUploadDialogService();
110        srv.setCms(CmsFlexController.getCmsObject(request));
111        srv.setRequest(request);
112        CmsPostUploadDialogBean result = null;
113        try {
114            result = srv.prefetch();
115        } finally {
116            srv.clearThreadStorage();
117        }
118        return result;
119    }
120
121    /**
122     * @see org.opencms.ade.postupload.shared.rpc.I_CmsPostUploadDialogService#load(org.opencms.util.CmsUUID, boolean,boolean)
123     */
124    public CmsPostUploadDialogPanelBean load(CmsUUID id, boolean useConfiguration, boolean addBasicProperties)
125    throws CmsRpcException {
126
127        try {
128            CmsResource res = getCmsObject().readResource(id);
129            List<CmsProperty> properties = getCmsObject().readPropertyObjects(res, false);
130            String title = CmsProperty.get(CmsPropertyDefinition.PROPERTY_TITLE, properties).getValue();
131            if (title == null) {
132                title = res.getName();
133            }
134            String description = CmsProperty.get(CmsPropertyDefinition.PROPERTY_DESCRIPTION, properties).getValue();
135            if (description == null) {
136                description = getCmsObject().getSitePath(res);
137            }
138            CmsListInfoBean listInfo = CmsVfsService.getPageInfo(getCmsObject(), res);
139
140            CmsPostUploadDialogPanelBean result = new CmsPostUploadDialogPanelBean(id, listInfo);
141            String warning = OpenCms.getADEManager().getUploadWarningTable().getMessage(res.getStructureId());
142            if (warning != null) {
143                Locale wpLocale = OpenCms.getWorkplaceManager().getWorkplaceLocale(getCmsObject());
144                CmsMacroResolver resolver = new CmsMacroResolver();
145                resolver.setMessages(OpenCms.getWorkplaceManager().getMessages(wpLocale));
146                warning = resolver.resolveMacros(warning);
147                result.setWarning(warning);
148            }
149
150            CmsObject cms = getCmsObject();
151            I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(res.getTypeId());
152            String typeName = type.getTypeName();
153            listInfo.setResourceType(typeName);
154
155            CmsExplorerTypeSettings settings = OpenCms.getWorkplaceManager().getExplorerTypeSetting(typeName);
156
157            CmsADEConfigData configData = OpenCms.getADEManager().lookupConfiguration(
158                getCmsObject(),
159                res.getRootPath());
160            Map<String, CmsXmlContentProperty> propertyConfiguration = configData.getPropertyConfigurationAsMap();
161
162            Set<String> propertiesToShow = new LinkedHashSet<String>();
163            {
164                List<String> defaultProperties = settings.getProperties();
165                while (defaultProperties.isEmpty() && !CmsStringUtil.isEmptyOrWhitespaceOnly(settings.getReference())) {
166                    settings = OpenCms.getWorkplaceManager().getExplorerTypeSetting(settings.getReference());
167                    defaultProperties = settings.getProperties();
168                }
169                propertiesToShow.addAll(defaultProperties);
170            }
171            if (addBasicProperties) {
172                propertiesToShow.addAll(propertyConfiguration.keySet());
173            }
174
175            Map<String, CmsXmlContentProperty> propertyDefinitions = new LinkedHashMap<String, CmsXmlContentProperty>();
176            Map<String, CmsClientProperty> clientProperties = new LinkedHashMap<String, CmsClientProperty>();
177
178            // match strings consisting of one or more alphanumeric characters and those from NAME_CONSTRAINTS, but exclude those that are just sequences of one or more "."s
179            String regex = "^(?!\\.+$)[" + CmsResource.NAME_CONSTRAINTS + "a-zA-Z0-9]+$";
180            Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms);
181            String validationMessage = Messages.get().getBundle(locale).key(
182                Messages.GUI_POSTUPLOAD_FILENAME_VALIDATION_ERROR_1,
183                CmsResource.NAME_CONSTRAINTS);
184            CmsXmlContentProperty fileNamePropDef = new CmsXmlContentProperty(
185                CmsPropertyModification.FILE_NAME_PROPERTY,
186                "string",
187                "string",
188                "",
189                regex,
190                "",
191                "",
192                Messages.get().getBundle(OpenCms.getWorkplaceManager().getWorkplaceLocale(getCmsObject())).key(
193                    Messages.GUI_UPLOAD_FILE_NAME_0),
194                "",
195                validationMessage,
196                "false");
197            propertyDefinitions.put(CmsPropertyModification.FILE_NAME_PROPERTY, fileNamePropDef);
198            clientProperties.put(
199                CmsPropertyModification.FILE_NAME_PROPERTY,
200                new CmsClientProperty(CmsPropertyModification.FILE_NAME_PROPERTY, res.getName(), res.getName()));
201
202            Set<String> requiredProperties = getRequiredProperties(getCmsObject(), res);
203            for (String propertyName : propertiesToShow) {
204                CmsXmlContentProperty propDef = null;
205                if (useConfiguration) {
206                    propDef = propertyConfiguration.get(propertyName);
207                }
208                if (propDef == null) {
209                    propDef = new CmsXmlContentProperty(
210                        propertyName,
211                        "string",
212                        "string",
213                        "",
214                        "",
215                        "",
216                        "",
217                        null,
218                        "",
219                        "",
220                        "false");
221                }
222                if (requiredProperties.contains(propertyName)) {
223                    String validationErrorMessage = Messages.get().getBundle(
224                        OpenCms.getWorkplaceManager().getWorkplaceLocale(getCmsObject())).key(
225                            Messages.GUI_POSTUPLOAD_REQUIRED_PROPERTY_1,
226                            propertyName);
227                    propDef = propDef.withValidation(".*?[^ ].*", "error", validationErrorMessage);
228                }
229
230                propertyDefinitions.put(propertyName, propDef);
231                CmsProperty property = CmsProperty.get(propertyName, properties);
232                if (property != null) {
233                    CmsClientProperty clientProperty = new CmsClientProperty(
234                        propertyName,
235                        property.getStructureValue(),
236                        property.getResourceValue());
237                    clientProperties.put(clientProperty.getName(), clientProperty);
238                }
239            }
240
241            propertyDefinitions = CmsXmlContentPropertyHelper.resolveMacrosInProperties(
242                propertyDefinitions,
243                CmsMacroResolver.newWorkplaceLocaleResolver(getCmsObject()));
244
245            CmsPropertyEditorHelper.updateWysiwygConfig(propertyDefinitions, getCmsObject(), res);
246
247            String previewLink = null;
248            if (CmsResourceTypeImage.getStaticTypeName().equals(typeName)) {
249                String extension = CmsResource.getExtension(res.getRootPath());
250                String suffix = extension != null ? "." + extension : "";
251                String permalink = CmsStringUtil.joinPaths(
252                    OpenCms.getSystemInfo().getOpenCmsContext(),
253                    CmsPermalinkResourceHandler.PERMALINK_HANDLER,
254                    res.getStructureId().toString()) + suffix;
255                previewLink = permalink + CmsGalleryFilePreview.getScaleQueryString(false);
256                result.setPermalink(permalink);
257                result.setPreviewLink(previewLink);
258                result.setHighResPreviewLink(permalink + CmsGalleryFilePreview.getScaleQueryString(true));
259                result.setPreviewInfo1((res.getLength() / 1024) + "kb");
260                CmsProperty imageSizeProp = cms.readPropertyObject(
261                    res,
262                    CmsPropertyDefinition.PROPERTY_IMAGE_SIZE,
263                    false);
264                String imageSizeText = "? x ?";
265                if (!CmsStringUtil.isEmptyOrWhitespaceOnly(imageSizeProp.getValue())) {
266                    Map<String, String> imageSizeAttrs = CmsStringUtil.splitAsMap(imageSizeProp.getValue(), ",", ":");
267                    String w = imageSizeAttrs.get("w");
268                    String h = imageSizeAttrs.get("h");
269                    if ((w != null) && (h != null)) {
270                        imageSizeText = w + " x " + h;
271
272                    }
273                }
274                result.setPreviewInfo2(imageSizeText);
275            }
276
277            result.setPropertyDefinitions(propertyDefinitions);
278            result.setProperties(clientProperties);
279            return result;
280        } catch (CmsException e) {
281            error(e);
282            return null; // will never be reached
283        }
284    }
285
286    /**
287     * @see org.opencms.ade.postupload.shared.rpc.I_CmsPostUploadDialogService#prefetch()
288     */
289    public CmsPostUploadDialogBean prefetch() throws CmsRpcException {
290
291        try {
292
293            List<CmsResource> resources = new ArrayList<>();
294
295            if ((CmsStringUtil.isNotEmptyOrWhitespaceOnly(
296                getRequest().getParameter(I_CmsDialogConstants.PARAM_RESOURCES)))) {
297                // if the request parameter resources exists and contains a list of UUIDs
298                // this dialog is used as upload hook
299                String resourcesParam = getRequest().getParameter(I_CmsDialogConstants.PARAM_RESOURCES);
300                List<String> resourceUUIDs = CmsStringUtil.splitAsList(resourcesParam, ",");
301                for (String uuidAsString : resourceUUIDs) {
302                    CmsUUID uuid = new CmsUUID(uuidAsString);
303                    CmsResource res = getCmsObject().readResource(uuid);
304                    resources.add(res);
305                }
306            } else if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(getRequest().getParameter("resource"))) {
307                // if there was no parameter "resources" set as request parameter
308                // this dialog is not used as upload hook try to read the resource parameter
309                String resourceParam = getRequest().getParameter("resource");
310                if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(resourceParam)) {
311                    CmsResource res = getCmsObject().readResource(resourceParam);
312                    resources.add(res);
313                }
314            }
315            return createUploadDialogBean(resources);
316        } catch (CmsException e) {
317            error(e);
318            return null; // will never be reached
319        }
320    }
321
322    /**
323     * @see org.opencms.ade.postupload.shared.rpc.I_CmsPostUploadDialogService#updateAllFiles(java.util.List, java.lang.String, java.lang.String, boolean, boolean)
324     */
325    @Override
326    public void updateAllFiles(
327        List<CmsUUID> ids,
328        String propName,
329        String propValue,
330        boolean overwriteNonEmpty,
331        boolean withBasicProperties)
332    throws CmsRpcException {
333
334        CmsObject cms = getCmsObject();
335        Exception caughtException = null;
336        for (CmsUUID id : ids) {
337            try {
338                CmsResource res = cms.readResource(id, CmsResourceFilter.IGNORE_EXPIRATION);
339
340                // most of this method body is trying to figure out which properties are applicable to which resources -
341                // a property value should only be updated if the corresponding property would actually show up in the dialog
342                // for that resource
343
344                I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(res.getTypeId());
345                String typeName = type.getTypeName();
346                CmsExplorerTypeSettings settings = OpenCms.getWorkplaceManager().getExplorerTypeSetting(typeName);
347                CmsADEConfigData configData = OpenCms.getADEManager().lookupConfigurationWithCache(
348                    getCmsObject(),
349                    CmsResource.getParentFolder(res.getRootPath()));
350                Map<String, CmsXmlContentProperty> propertyConfiguration = configData.getPropertyConfigurationAsMap();
351                Set<String> editableProperties = new LinkedHashSet<String>();
352                {
353                    List<String> defaultProperties = settings.getProperties();
354                    while (defaultProperties.isEmpty()
355                        && !CmsStringUtil.isEmptyOrWhitespaceOnly(settings.getReference())) {
356                        settings = OpenCms.getWorkplaceManager().getExplorerTypeSetting(settings.getReference());
357                        defaultProperties = settings.getProperties();
358                    }
359                    editableProperties.addAll(defaultProperties);
360                }
361                if (withBasicProperties) {
362                    editableProperties.addAll(propertyConfiguration.keySet());
363                }
364                if (editableProperties.contains(propName)) {
365                    Map<String, CmsProperty> existingProps = CmsProperty.toObjectMap(
366                        cms.readPropertyObjects(res, false));
367                    boolean write = false;
368                    if (!existingProps.containsKey(propName)) {
369                        write = true;
370                    } else {
371                        write = overwriteNonEmpty
372                            && !Objects.equals(existingProps.get(propName).getStructureValue(), propValue);
373                    }
374                    if (write) {
375                        try (AutoCloseable c = CmsLockUtil.withLockedResources(cms, res)) {
376                            CmsProperty newProp = new CmsProperty(propName, propValue, null);
377                            cms.writePropertyObject(cms.getSitePath(res), newProp);
378                        } catch (CmsException e) {
379                            throw e;
380                        } catch (Exception e) {
381                            LOG.error(e.getLocalizedMessage(), e);
382                        }
383                    }
384                }
385            } catch (Exception e) {
386                LOG.error(e.getLocalizedMessage(), e);
387                if (caughtException == null) {
388                    caughtException = e;
389                }
390            }
391        }
392        if (caughtException != null) {
393            error(caughtException);
394        }
395    }
396
397    /**
398     * Creates the data bean for the dialog from the list of created resources.
399     *
400     * @param resources the resources
401     * @return the data bean for the dialog
402     */
403    private CmsPostUploadDialogBean createUploadDialogBean(List<CmsResource> resources) {
404
405        Map<CmsUUID, String> result = new LinkedHashMap<>();
406        CmsObject cms = getCmsObject();
407        boolean hasImage = false;
408        for (CmsResource resource : resources) {
409            if (OpenCms.getResourceManager().matchResourceType(
410                CmsResourceTypeImage.getStaticTypeName(),
411                resource.getTypeId())) {
412                hasImage = true;
413                break;
414            }
415        }
416        // split resource list into two parts, ones that have required properties and ones that don't,
417        // then iterate over the ones with required properties first.
418        //
419        // this is because the buttons in the upload property dialog only trigger validation for the current tab,
420        // so we want the user to go through all resources which require validation first before they can exit the dialog.
421        Map<Boolean, List<CmsResource>> parts = resources.stream().collect(
422            Collectors.partitioningBy(res -> getRequiredProperties(cms, res).size() > 0));
423
424        for (CmsResource res : Iterables.concat(parts.get(Boolean.TRUE), parts.get(Boolean.FALSE))) {
425            result.put(res.getStructureId(), cms.getRequestContext().removeSiteRoot(res.getRootPath()));
426        }
427        Set<CmsUUID> reqValIds = parts.get(Boolean.TRUE).stream().map(res -> res.getStructureId()).collect(
428            Collectors.toSet());
429        return new CmsPostUploadDialogBean(result, reqValIds, hasImage);
430    }
431
432    /**
433     * Gets the properties required for the given resource (as defined by the requiredOnUpload setting on the corresponding explorertype).
434     *
435     * @param cms the CMS context
436     * @param res a resource
437     * @return the set of required properties
438     */
439    private Set<String> getRequiredProperties(CmsObject cms, CmsResource res) {
440
441        Set<String> requiredProps = new HashSet<>();
442        try {
443            String typeName = OpenCms.getResourceManager().getResourceType(res).getTypeName();
444            CmsExplorerTypeSettings explorerType = OpenCms.getWorkplaceManager().getExplorerTypeSetting(typeName);
445            if (explorerType != null) {
446                for (String prop : explorerType.getProperties()) {
447                    if (explorerType.isPropertyRequiredOnUpload(prop)) {
448                        requiredProps.add(prop);
449                    }
450                }
451            }
452        } catch (Exception e) {
453            LOG.error(e.getLocalizedMessage(), e);
454            return requiredProps;
455        }
456        return requiredProps;
457    }
458
459}