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.lang.reflect.Array;
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;
026
027import javax.jcr.Node;
028import javax.jcr.RepositoryException;
029import javax.jcr.lock.LockManager;
030
031import org.apache.avalon.framework.activity.Initializable;
032import org.apache.avalon.framework.parameters.Parameters;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.cocoon.ProcessingException;
036import org.apache.cocoon.acting.ServiceableAction;
037import org.apache.cocoon.environment.ObjectModelHelper;
038import org.apache.cocoon.environment.Redirector;
039import org.apache.cocoon.environment.Request;
040import org.apache.cocoon.environment.SourceResolver;
041import org.apache.commons.lang3.StringUtils;
042import org.slf4j.Logger;
043
044import org.ametys.cms.CmsConstants;
045import org.ametys.cms.lock.LockContentManager;
046import org.ametys.cms.model.restrictions.RestrictedModelItem;
047import org.ametys.cms.repository.Content;
048import org.ametys.core.cocoon.JSonReader;
049import org.ametys.core.user.CurrentUserProvider;
050import org.ametys.core.user.UserIdentity;
051import org.ametys.core.util.AvalonLoggerAdapter;
052import org.ametys.plugins.core.ui.help.HelpManager;
053import org.ametys.plugins.core.user.UserHelper;
054import org.ametys.plugins.repository.AmetysObject;
055import org.ametys.plugins.repository.AmetysObjectResolver;
056import org.ametys.plugins.repository.AmetysRepositoryException;
057import org.ametys.plugins.repository.jcr.JCRAmetysObject;
058import org.ametys.plugins.repository.lock.LockHelper;
059import org.ametys.plugins.repository.lock.LockableAmetysObject;
060import org.ametys.plugins.repository.version.VersionableAmetysObject;
061import org.ametys.runtime.model.DefinitionContext;
062import org.ametys.runtime.model.ElementDefinition;
063import org.ametys.runtime.model.Model;
064import org.ametys.runtime.model.ModelItem;
065import org.ametys.runtime.model.type.DataContext;
066import org.ametys.runtime.model.type.ElementType;
067import org.ametys.web.renderingcontext.RenderingContext;
068import org.ametys.web.renderingcontext.RenderingContextHandler;
069
070/**
071 * Check if the content can be edited, and return the value
072 */
073public class GetServerValuesAction extends ServiceableAction implements Initializable
074{
075    /** The logger */
076    protected Logger _logger;
077    /** The ametys object resolver */
078    protected AmetysObjectResolver _resolver;
079    /** The current user provider */
080    protected CurrentUserProvider _currentUserProvider;
081    /** The rendering context handler */
082    protected RenderingContextHandler _renderingContextHandler;
083    /** User helper */
084    protected UserHelper _userHelper;
085    /** Lock Content Manager */
086    protected LockContentManager _lockContentManager;
087    /** The help manager to get url for each property */
088    protected HelpManager _helpManager;
089    
090    @Override
091    public void service(ServiceManager serviceManager) throws ServiceException
092    {
093        super.service(serviceManager);
094        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
095        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
096        _renderingContextHandler = (RenderingContextHandler) serviceManager.lookup(RenderingContextHandler.ROLE);
097        _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE);
098        _lockContentManager = (LockContentManager) serviceManager.lookup(LockContentManager.ROLE);
099        _helpManager = (HelpManager) serviceManager.lookup(HelpManager.ROLE);
100    }
101    
102    public void initialize() throws Exception
103    {
104        _logger = new AvalonLoggerAdapter(getLogger());
105    }
106
107    @Override
108    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
109    {
110        Request request = ObjectModelHelper.getRequest(objectModel);
111
112        String contentId = parameters.getParameter("contentId");
113        String attributePathsAsString = parameters.getParameter("metadataPaths");
114        String workflowIdsAsString = parameters.getParameter("workflowIds", null);
115
116        Map<String, Object> jsonObject = new HashMap<>();
117        boolean success = true;
118
119        if (attributePathsAsString == null)
120        {
121            success = false;
122            jsonObject.put("error", "no metadata");
123            request.setAttribute(JSonReader.OBJECT_TO_READ, jsonObject);
124            return EMPTY_MAP;
125        }
126        List<String> attributePaths = Arrays.asList(attributePathsAsString.split(";"));
127
128        boolean validateContent = parameters.getParameterAsBoolean("validateContent", false);
129
130        boolean isEditionMode = "true".equals(request.getParameter("_edition"));
131
132        validateContent &= !isEditionMode; //no validation if in edition mode
133
134        Content content = _resolver.resolveById(contentId);
135        // lock validation
136        UserIdentity locker = isContentLocked(content);
137        if (locker != null)
138        {
139            success = false;
140            String userFullName = _userHelper.getUserFullName(locker);
141            jsonObject.put("error", "locked");
142            Map<String, String> userIdentyJson = new HashMap<>();
143            userIdentyJson.put("fullName", userFullName);
144            jsonObject.put("locker", userIdentyJson);
145        }
146        else if (validateContent)
147        {
148            // draft/live validation
149            RenderingContext context = _renderingContextHandler.getRenderingContext();
150            if (context == RenderingContext.FRONT && content instanceof VersionableAmetysObject)
151            {
152                String[] labels = ((VersionableAmetysObject) content).getLabels();
153                if (!Arrays.asList(labels).contains(CmsConstants.LIVE_LABEL))
154                {
155                    success = false;
156                    jsonObject.put("error", "draft");
157                }
158            }
159        }
160
161        // workflow validation
162        if (success)
163        {
164            if (workflowIdsAsString == null)
165            {
166                success = false;
167                jsonObject.put("error", "no workflow Ids");
168                request.setAttribute(JSonReader.OBJECT_TO_READ, jsonObject);
169                return EMPTY_MAP;
170            }
171            List<String> workflowIdsAsStrings = Arrays.asList(workflowIdsAsString.split(";"));
172            List<Integer> workflowIds = new ArrayList<>();
173            for (String workflowIdAsString : workflowIdsAsStrings)
174            {
175                workflowIds.add(Integer.parseInt(workflowIdAsString));
176            }
177            boolean workflowRightsOk = AmetysFrontEditionHelper.hasWorkflowRight(workflowIds, contentId, false);
178            if (!workflowRightsOk)
179            {
180                success = false;
181                jsonObject.put("error", "workflow-rights");
182                request.setAttribute(JSonReader.OBJECT_TO_READ, jsonObject);
183                return EMPTY_MAP;
184            }
185        }
186
187        if (success)
188        {
189            List<String> contentIds = new ArrayList<>(1);
190            contentIds.add(contentId);
191            _lockContentManager.unlockOrLock(contentIds, "lock");
192            List<String> languages = getLanguages(request);
193            
194            Map<String, Object> attributeJsonObject = new HashMap<>();
195            for (String attributePath : attributePaths)
196            {
197                ModelItem modelItem = getDefinition(content, attributePath);
198                if (checkRestriction(content, modelItem))
199                {
200                    Map<String, Object> contentAttribute2Json = _contentAttribute2Json(content, modelItem, attributePath, languages);
201                    attributeJsonObject.put(attributePath, contentAttribute2Json);
202                }
203            }
204            jsonObject.put("data", attributeJsonObject);
205        }
206
207        request.setAttribute(JSonReader.OBJECT_TO_READ, jsonObject);
208        return EMPTY_MAP;
209    }
210    
211    /**
212     * Check if attribute can be edited
213     * @param content the content
214     * @param modelItem the definition of attribute
215     * @return true if the attribute can be edited
216     */
217    @SuppressWarnings("unchecked")
218    protected boolean checkRestriction(Content content, ModelItem modelItem)
219    {
220        if (modelItem instanceof RestrictedModelItem)
221        {
222            return ((RestrictedModelItem) modelItem).canWrite(content);
223        }
224        return true; // no restriction
225    }
226
227    /**
228     * Check if the content is locked
229     * @param content The content
230     * @return UserIdentity of the locker, of null if not locked
231     */
232    protected UserIdentity isContentLocked(Content content)
233    {
234        if (!(content instanceof JCRAmetysObject))
235        {
236            return null;
237        }
238
239        try
240        {
241            Node node = ((JCRAmetysObject) content).getNode();
242            LockManager lockManager = node.getSession().getWorkspace().getLockManager();
243
244            if (lockManager.isLocked(node.getPath()))
245            {
246                Node lockHolder = lockManager.getLock(node.getPath()).getNode();
247
248                AmetysObject ao = _resolver.resolve(lockHolder, false);
249                if (ao instanceof LockableAmetysObject)
250                {
251                    LockableAmetysObject lockableAO = (LockableAmetysObject) ao;
252                    if (!LockHelper.isLockOwner(lockableAO, _currentUserProvider.getUser()))
253                    {
254                        return lockableAO.getLockOwner();
255                    }
256                }
257            }
258        }
259        catch (RepositoryException e)
260        {
261            getLogger().error(String.format("Repository exception during lock checking for ametys object '%s'", content.getId()), e);
262            throw new AmetysRepositoryException(e);
263        }
264        return null;
265    }
266    
267    /**
268     * list all languages requested by the client in the request
269     * @param request the request
270     * @return an ordered list of all languages requested by the client (or server default locale if none requested by the client)
271     */
272    protected List<String> getLanguages(Request request)
273    {
274        Enumeration locales = request.getLocales();
275        List<String> languages = new ArrayList<>();
276        while (locales.hasMoreElements())
277        {
278            Locale locale = (Locale) locales.nextElement();
279            String lang = locale.getLanguage();
280            if (!languages.contains(lang))
281            {
282                languages.add(lang);
283            }
284        }
285        return languages;
286    }
287    
288    /**
289     * Get the definition to the given attribute path
290     * @param content the content
291     * @param attributePath the path of the attribute
292     * @return the model item
293     * @throws ProcessingException if the attribute is not defined by the model
294     * @throws AmetysRepositoryException if an error occurred
295     */
296    protected ModelItem getDefinition(Content content, String attributePath) throws ProcessingException, AmetysRepositoryException
297    {
298        if (!content.hasDefinition(attributePath))
299        {
300            throw new ProcessingException(String.format("Unknown attribute path '%s' for content type(s) '%s'", attributePath, StringUtils.join(content.getTypes(), ',')));
301        }
302        
303        return content.getDefinition(attributePath);
304    }
305    
306    /**
307     * Convert the content attribute at the given path into a JSON object
308     * @param content the content
309     * @param modelItem the attribute definition
310     * @param attributePath the path of the attribute to convert
311     * @param languages all languages requested by the client
312     * @return the attribute as a JSON object
313     * @throws ProcessingException if an error occurs 
314     */
315    @SuppressWarnings("unchecked")
316    protected Map<String, Object> _contentAttribute2Json(Content content, ModelItem modelItem, String attributePath, List<String> languages) throws ProcessingException
317    {
318        Map<String, Object> jsonObject = modelItem.toJSON(DefinitionContext.newInstance().withObject(content));
319        
320        String help = _getModelItemHelpLink(modelItem, languages);
321        if (StringUtils.isNotBlank(help))
322        {
323            jsonObject.put("help", help);
324        }
325        
326        if (modelItem instanceof ElementDefinition && (!(modelItem instanceof RestrictedModelItem) || ((RestrictedModelItem<Content>) modelItem).canRead(content)))
327        {
328            Object value = content.getValue(attributePath);
329
330            if (value != null)
331            {
332                ElementType type = (ElementType) modelItem.getType();
333                DataContext context = DataContext.newInstance()
334                        .withObjectId(content.getId())
335                        .withDataPath(attributePath);
336                
337                Object valueAsJSON = type.valueToJSONForEdition(value, context);
338                if (valueAsJSON instanceof List)
339                {
340                    if (!((List) valueAsJSON).isEmpty())
341                    {
342                        Object[] arrayValue = (Object[]) Array.newInstance(((List) valueAsJSON).get(0).getClass(), ((List) valueAsJSON).size());
343                        jsonObject.put("value", ((List) valueAsJSON).toArray(arrayValue));
344                    }
345                    else
346                    {
347                        jsonObject.put("value", new Object[0]);
348                    }
349                }
350                else
351                {
352                    jsonObject.put("value", valueAsJSON);
353                }
354            }
355        }
356        
357        return jsonObject;
358    }
359    
360    /**
361     * Retrieves the Help link for the given model item
362     * @param modelItem the model item
363     * @param languages all languages requested by the client
364     * @return the help link
365     */
366    protected String _getModelItemHelpLink(ModelItem modelItem, List<String> languages)
367    {
368        Model model = modelItem.getModel();
369        if (model != null)
370        {
371            String modelId = model.getId();
372            String family = model.getFamilyId();
373            
374            String path = modelItem.getPath();
375            //If there is a path, and it does not starts with '/', we add one at the beginning
376            if (StringUtils.isNotBlank(path))
377            {
378                path = StringUtils.prependIfMissing(path, ModelItem.ITEM_PATH_SEPARATOR);
379            }
380            String featureId = StringUtils.join(modelId, path);
381            //Remove the starting '/' if present
382            featureId = StringUtils.removeStart(featureId, ModelItem.ITEM_PATH_SEPARATOR);
383
384            try
385            {
386                return _helpManager.getHelp(family, featureId, languages);
387            }
388            catch (Exception e)
389            {
390                _logger.warn("Impossible to get help for the content type '{}' on path '{}'", modelId, path, e);
391            }
392        }
393           
394        return StringUtils.EMPTY;
395    }
396}