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}