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