001/*
002 *  Copyright 2017 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.frontedition;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Enumeration;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026import java.util.stream.Collectors;
027
028import javax.jcr.Node;
029import javax.jcr.RepositoryException;
030import javax.jcr.lock.LockManager;
031
032import org.apache.avalon.framework.activity.Initializable;
033import org.apache.avalon.framework.parameters.Parameters;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.cocoon.ProcessingException;
037import org.apache.cocoon.acting.ServiceableAction;
038import org.apache.cocoon.environment.ObjectModelHelper;
039import org.apache.cocoon.environment.Redirector;
040import org.apache.cocoon.environment.Request;
041import org.apache.cocoon.environment.SourceResolver;
042import org.apache.commons.lang3.StringUtils;
043import org.slf4j.Logger;
044
045import org.ametys.cms.content.ContentHelper;
046import org.ametys.cms.contenttype.ContentTypesHelper;
047import org.ametys.cms.contenttype.MetadataDefinition;
048import org.ametys.cms.data.type.ModelItemTypeConstants;
049import org.ametys.cms.lock.LockContentManager;
050import org.ametys.cms.model.restrictions.RestrictedModelItem;
051import org.ametys.cms.repository.Content;
052import org.ametys.cms.transformation.RichTextTransformer;
053import org.ametys.core.cocoon.JSonReader;
054import org.ametys.core.user.CurrentUserProvider;
055import org.ametys.core.user.UserIdentity;
056import org.ametys.core.util.AvalonLoggerAdapter;
057import org.ametys.plugins.core.ui.help.HelpManager;
058import org.ametys.plugins.core.user.UserHelper;
059import org.ametys.plugins.explorer.resources.Resource;
060import org.ametys.plugins.repository.AmetysObject;
061import org.ametys.plugins.repository.AmetysObjectResolver;
062import org.ametys.plugins.repository.AmetysRepositoryException;
063import org.ametys.plugins.repository.jcr.JCRAmetysObject;
064import org.ametys.plugins.repository.lock.LockHelper;
065import org.ametys.plugins.repository.lock.LockableAmetysObject;
066import org.ametys.plugins.repository.metadata.BinaryMetadata;
067import org.ametys.plugins.repository.metadata.ModifiableRichText;
068import org.ametys.plugins.repository.version.VersionableAmetysObject;
069import org.ametys.runtime.model.DefinitionContext;
070import org.ametys.runtime.model.ElementDefinition;
071import org.ametys.runtime.model.Model;
072import org.ametys.runtime.model.ModelItem;
073import org.ametys.runtime.parameter.ParameterHelper;
074import org.ametys.web.renderingcontext.RenderingContext;
075import org.ametys.web.renderingcontext.RenderingContextHandler;
076
077/**
078 * Check if the content can be edited, and return the value
079 */
080public class GetServerValuesAction extends ServiceableAction implements Initializable
081{
082    /** The logger */
083    protected Logger _logger;
084    /** The ametys object resolver */
085    protected AmetysObjectResolver _resolver;
086    /** The current user provider */
087    protected CurrentUserProvider _currentUserProvider;
088    /** The rendering context handler */
089    protected RenderingContextHandler _renderingContextHandler;
090    /** Helper for content type */
091    protected ContentTypesHelper _contentTypesHelper;
092    /** The content helper */
093    protected ContentHelper _contentHelper;
094    /** User helper */
095    protected UserHelper _userHelper;
096    /** Lock Content Manager */
097    protected LockContentManager _lockContentManager;
098    /** Our metatadata manager for file */
099    protected FileMetadataManager _fileMetadataManager;
100    /** The help manager to get url for each property */
101    protected HelpManager _helpManager;
102    
103    @Override
104    public void service(ServiceManager serviceManager) throws ServiceException
105    {
106        super.service(serviceManager);
107        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
108        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
109        _renderingContextHandler = (RenderingContextHandler) serviceManager.lookup(RenderingContextHandler.ROLE);
110        _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
111        _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE);
112        _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE);
113        _lockContentManager = (LockContentManager) serviceManager.lookup(LockContentManager.ROLE);
114        _fileMetadataManager = (FileMetadataManager) serviceManager.lookup(FileMetadataManager.ROLE);
115        _helpManager = (HelpManager) serviceManager.lookup(HelpManager.ROLE);
116    }
117    
118    public void initialize() throws Exception
119    {
120        _logger = new AvalonLoggerAdapter(getLogger());
121    }
122
123    @Override
124    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
125    {
126        Request request = ObjectModelHelper.getRequest(objectModel);
127
128        String contentId = parameters.getParameter("contentId");
129        String metadataPathsAsString = parameters.getParameter("metadataPaths");
130        String workflowIdsAsString = parameters.getParameter("workflowIds", null);
131
132        Map<String, Object> jsonObject = new HashMap<>();
133        boolean success = true;
134
135        if (metadataPathsAsString == null)
136        {
137            success = false;
138            jsonObject.put("error", "no metadata");
139            request.setAttribute(JSonReader.OBJECT_TO_READ, jsonObject);
140            return EMPTY_MAP;
141        }
142        List<String> metadataPaths = Arrays.asList(metadataPathsAsString.split(";"));
143
144        boolean validateContent = parameters.getParameterAsBoolean("validateContent", false);
145
146        boolean isEditionMode = "true".equals(request.getParameter("_edition"));
147
148        validateContent &= !isEditionMode; //no validation if in edition mode
149
150        Content content = _resolver.resolveById(contentId);
151        // lock validation
152        UserIdentity locker = isContentLocked(content);
153        if (locker != null)
154        {
155            success = false;
156            String userFullName = _userHelper.getUserFullName(locker);
157            jsonObject.put("error", "locked");
158            Map<String, String> userIdentyJson = new HashMap<>();
159            userIdentyJson.put("fullName", userFullName);
160            jsonObject.put("locker", userIdentyJson);
161        }
162        else if (validateContent)
163        {
164            // draft/live validation
165            RenderingContext context = _renderingContextHandler.getRenderingContext();
166            if (context == RenderingContext.FRONT && content instanceof VersionableAmetysObject)
167            {
168                String[] labels = ((VersionableAmetysObject) content).getLabels();
169                if (!Arrays.asList(labels).contains("Live"))
170                {
171                    success = false;
172                    jsonObject.put("error", "draft");
173                }
174            }
175        }
176
177        // workflow validation
178        if (success)
179        {
180            if (workflowIdsAsString == null)
181            {
182                success = false;
183                jsonObject.put("error", "no workflow Ids");
184                request.setAttribute(JSonReader.OBJECT_TO_READ, jsonObject);
185                return EMPTY_MAP;
186            }
187            List<String> workflowIdsAsStrings = Arrays.asList(workflowIdsAsString.split(";"));
188            List<Integer> workflowIds = new ArrayList<>();
189            for (String workflowIdAsString : workflowIdsAsStrings)
190            {
191                workflowIds.add(Integer.parseInt(workflowIdAsString));
192            }
193            boolean workflowRightsOk = AmetysFrontEditionHelper.hasWorkflowRight(workflowIds, contentId, false);
194            if (!workflowRightsOk)
195            {
196                success = false;
197                jsonObject.put("error", "workflow-rights");
198                request.setAttribute(JSonReader.OBJECT_TO_READ, jsonObject);
199                return EMPTY_MAP;
200            }
201        }
202
203        if (success)
204        {
205            List<String> contentIds = new ArrayList<>(1);
206            contentIds.add(contentId);
207            _lockContentManager.unlockOrLock(contentIds, "lock");
208            List<String> languages = getLanguages(request);
209            
210            Map<String, Object> metadataJsonObject = new HashMap<>();
211            for (String metadataPath : metadataPaths)
212            {
213                Map<String, Object> contentMetadata2Json = _contentAttribute2Json(content, metadataPath, languages);
214                metadataJsonObject.put(metadataPath, contentMetadata2Json);
215            }
216            jsonObject.put("data", metadataJsonObject);
217        }
218
219        request.setAttribute(JSonReader.OBJECT_TO_READ, jsonObject);
220        return EMPTY_MAP;
221    }
222
223    /**
224     * Check if the content is locked
225     * @param content The content
226     * @return UserIdentity of the locker, of null if not locked
227     */
228    protected UserIdentity isContentLocked(Content content)
229    {
230        if (!(content instanceof JCRAmetysObject))
231        {
232            return null;
233        }
234
235        try
236        {
237            Node node = ((JCRAmetysObject) content).getNode();
238            LockManager lockManager = node.getSession().getWorkspace().getLockManager();
239
240            if (lockManager.isLocked(node.getPath()))
241            {
242                Node lockHolder = lockManager.getLock(node.getPath()).getNode();
243
244                AmetysObject ao = _resolver.resolve(lockHolder, false);
245                if (ao instanceof LockableAmetysObject)
246                {
247                    LockableAmetysObject lockableAO = (LockableAmetysObject) ao;
248                    if (!LockHelper.isLockOwner(lockableAO, _currentUserProvider.getUser()))
249                    {
250                        return lockableAO.getLockOwner();
251                    }
252                }
253            }
254        }
255        catch (RepositoryException e)
256        {
257            getLogger().error(String.format("Repository exception during lock checking for ametys object '%s'", content.getId()), e);
258            throw new AmetysRepositoryException(e);
259        }
260        return null;
261    }
262    
263    /**
264     * list all languages requested by the client in the request
265     * @param request the request
266     * @return an ordonned list of all languages requested by the client (or server default locale if none requested by the client)
267     */
268    protected List<String> getLanguages(Request request)
269    {
270        Enumeration locales = request.getLocales();
271        List<String> languages = new ArrayList<>();
272        while (locales.hasMoreElements())
273        {
274            Locale locale = (Locale) locales.nextElement();
275            String lang = locale.getLanguage();
276            if (!languages.contains(lang))
277            {
278                languages.add(lang);
279            }
280        }
281        return languages;
282    }
283    
284    @SuppressWarnings("unchecked")
285    private Map<String, Object> _contentAttribute2Json(Content content, String attributePath, List<String> languages) throws ProcessingException
286    {
287        if (!content.hasDefinition(attributePath))
288        {
289            throw new ProcessingException(String.format("Unknown attribute path '%s' for content type(s) '%s'", attributePath, StringUtils.join(content.getTypes(), ',')));
290        }
291        
292        ModelItem modelItem = content.getDefinition(attributePath);
293        Map<String, Object> jsonObject = modelItem.toJSON(DefinitionContext.newInstance().withObject(content));
294        
295        String help = _getModelItemHelpLink(modelItem, languages);
296        if (StringUtils.isNotBlank(help))
297        {
298            jsonObject.put("help", help);
299        }
300        
301        if (!(modelItem instanceof RestrictedModelItem) || ((RestrictedModelItem<Content>) modelItem).canRead(content))
302        {
303            List<Object> values = _contentHelper.getMetadataValues(content, attributePath, new Locale(content.getLanguage()), false, false);
304            
305            if (values.size() > 0)
306            {
307                if (ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID.equals(modelItem.getType().getId()))
308                {
309                    ModifiableRichText value = (ModifiableRichText) values.get(0);
310                    
311                    StringBuilder result = new StringBuilder(2048);
312                    try
313                    {
314                        MetadataDefinition metadataDef = _contentTypesHelper.getMetadataDefinition(attributePath, content);
315                        RichTextTransformer richTextTransformer = metadataDef.getRichTextTransformer();
316                        richTextTransformer.transformForEditing(value, result);
317                        jsonObject.put("value", result.toString());
318                    }
319                    catch (IOException e)
320                    {
321                        throw new AmetysRepositoryException("Unable to transform a rich text into a string", e);
322                    }
323                }
324                else if (ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID.equals(modelItem.getType().getId()))
325                {
326                    Object value = values.get(0);
327                    
328                    if (value instanceof BinaryMetadata)
329                    {
330                        jsonObject.put("value", _fileMetadataManager.readBinaryMetadata(content, (BinaryMetadata) value, attributePath));
331                    }
332                    else if (value instanceof Resource)
333                    {
334                        jsonObject.put("value", _fileMetadataManager.readResource((Resource) value));
335                    }
336                    else if (value instanceof String)
337                    {
338                        try
339                        {
340                            Resource resource = _resolver.resolveById((String) value);
341                            jsonObject.put("value", _fileMetadataManager.readResource(resource));
342                        }
343                        catch (AmetysRepositoryException e)
344                        {
345                            getLogger().info("Cannot sax existing value for metadata '" + attributePath + "' of content '" + content.getId() + "'. The user will see an empty value", e);
346                        }
347                    }
348                }
349                else if (modelItem instanceof ElementDefinition)
350                {
351                    List<String> valuesAsStr = values.stream()
352                        .map(v -> ParameterHelper.valueToString(v))
353                        .collect(Collectors.toList());
354                    
355                    jsonObject.put("value", ((ElementDefinition) modelItem).isMultiple() ? valuesAsStr.toArray(new String[valuesAsStr.size()]) : valuesAsStr.get(0));
356                }
357            }
358        }
359        
360        return jsonObject;
361    }
362    
363    private String _getModelItemHelpLink(ModelItem modelItem, List<String> languages)
364    {
365        Model model = modelItem.getModel();
366        if (model != null)
367        {
368            String modelId = model.getId();
369            String family = model.getFamilyId();
370            
371            String path = modelItem.getPath();
372            //If there is a path, and it does not starts with '/', we add one at the beginning
373            if (StringUtils.isNotBlank(path))
374            {
375                path = StringUtils.prependIfMissing(path, ModelItem.ITEM_PATH_SEPARATOR);
376            }
377            String featureId = StringUtils.join(modelId, path);
378            //Remove the starting '/' if present
379            featureId = StringUtils.removeStart(featureId, ModelItem.ITEM_PATH_SEPARATOR);
380
381            try
382            {
383                return _helpManager.getHelp(family, featureId, languages);
384            }
385            catch (Exception e)
386            {
387                _logger.warn("Impossible to get help for the content type '{}' on path '{}'", modelId, path, e);
388            }
389        }
390           
391        return StringUtils.EMPTY;
392    }
393}