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}