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}