001/* 002 * Copyright 2015 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.cms.repository; 017 018import java.time.ZonedDateTime; 019import java.util.ArrayList; 020import java.util.Date; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.concurrent.ExecutionException; 027import java.util.concurrent.Future; 028import java.util.stream.Collectors; 029 030import javax.jcr.Node; 031import javax.jcr.RepositoryException; 032import javax.jcr.Session; 033 034import org.apache.avalon.framework.component.Component; 035import org.apache.avalon.framework.context.Context; 036import org.apache.avalon.framework.context.ContextException; 037import org.apache.avalon.framework.context.Contextualizable; 038import org.apache.avalon.framework.logger.AbstractLogEnabled; 039import org.apache.avalon.framework.service.ServiceException; 040import org.apache.avalon.framework.service.ServiceManager; 041import org.apache.avalon.framework.service.Serviceable; 042import org.apache.cocoon.components.ContextHelper; 043import org.apache.cocoon.environment.Request; 044import org.apache.commons.lang3.StringUtils; 045import org.apache.commons.lang3.tuple.Pair; 046import org.slf4j.Logger; 047 048import org.ametys.cms.ObservationConstants; 049import org.ametys.cms.content.ContentHelper; 050import org.ametys.cms.content.archive.ArchiveConstants; 051import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper; 052import org.ametys.cms.contenttype.ContentType; 053import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 054import org.ametys.cms.contenttype.ContentTypesHelper; 055import org.ametys.cms.contenttype.MetadataSet; 056import org.ametys.cms.data.ContentDataHelper; 057import org.ametys.cms.data.type.ModelItemTypeConstants; 058import org.ametys.cms.lock.LockContentManager; 059import org.ametys.cms.repository.ReactionableObject.ReactionType; 060import org.ametys.cms.tag.CMSTag; 061import org.ametys.cms.tag.TagProviderExtensionPoint; 062import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 063import org.ametys.cms.workflow.ContentWorkflowHelper; 064import org.ametys.cms.workflow.EditContentFunction; 065import org.ametys.core.observation.Event; 066import org.ametys.core.observation.ObservationManager; 067import org.ametys.core.right.RightManager; 068import org.ametys.core.right.RightManager.RightResult; 069import org.ametys.core.ui.Callable; 070import org.ametys.core.user.CurrentUserProvider; 071import org.ametys.core.user.UserIdentity; 072import org.ametys.core.user.UserManager; 073import org.ametys.core.util.DateUtils; 074import org.ametys.plugins.core.user.UserHelper; 075import org.ametys.plugins.explorer.ExplorerNode; 076import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 077import org.ametys.plugins.explorer.resources.Resource; 078import org.ametys.plugins.explorer.resources.ResourceCollection; 079import org.ametys.plugins.repository.AmetysObject; 080import org.ametys.plugins.repository.AmetysObjectIterable; 081import org.ametys.plugins.repository.AmetysObjectResolver; 082import org.ametys.plugins.repository.AmetysRepositoryException; 083import org.ametys.plugins.repository.CopiableAmetysObject; 084import org.ametys.plugins.repository.ModifiableAmetysObject; 085import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 086import org.ametys.plugins.repository.RemovableAmetysObject; 087import org.ametys.plugins.repository.TraversableAmetysObject; 088import org.ametys.plugins.repository.UnknownAmetysObjectException; 089import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder; 090import org.ametys.plugins.repository.lock.LockAwareAmetysObject; 091import org.ametys.plugins.repository.lock.LockHelper; 092import org.ametys.plugins.repository.lock.LockableAmetysObject; 093import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 094import org.ametys.plugins.repository.version.ModifiableDataAwareVersionableAmetysObject; 095import org.ametys.plugins.repository.version.VersionableAmetysObject; 096import org.ametys.plugins.workflow.AbstractWorkflowComponent; 097import org.ametys.plugins.workflow.component.CheckRightsCondition; 098import org.ametys.plugins.workflow.store.AbstractJackrabbitWorkflowStore; 099import org.ametys.plugins.workflow.store.AmetysObjectWorkflowStore; 100import org.ametys.plugins.workflow.support.WorkflowProvider; 101import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 102import org.ametys.runtime.authentication.AccessDeniedException; 103 104import com.opensymphony.workflow.WorkflowException; 105import com.opensymphony.workflow.spi.Step; 106 107/** 108 * DAO for manipulating contents 109 * 110 */ 111public class ContentDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 112{ 113 /** Avalon Role */ 114 public static final String ROLE = ContentDAO.class.getName(); 115 116 /** Deletion status : deleted */ 117 protected static final String _CONTENT_DELETION_STATUS_DELETED = "deleted"; 118 119 /** Deletion status : undeleted */ 120 protected static final String _CONTENT_DELETION_STATUS_UNDELETED = "undeleted"; 121 122 /** Deletion status : referenced */ 123 protected static final String _CONTENT_DELETION_STATUS_REFERENCED = "referenced"; 124 125 /** Deletion status : unauthorized */ 126 protected static final String _CONTENT_DELETION_STATUS_UNAUTHORIZED = "unauthorized"; 127 128 /** Deletion status : locked */ 129 protected static final String _CONTENT_DELETION_STATUS_LOCKED = "locked"; 130 131 /** Ametys resolver */ 132 protected AmetysObjectResolver _resolver; 133 /** Ametys observation manger */ 134 protected ObservationManager _observationManager; 135 /** Component to get current user */ 136 protected CurrentUserProvider _currentUserProvider; 137 /** Component to get tags */ 138 protected TagProviderExtensionPoint _tagProvider; 139 140 /** Workflow component */ 141 protected WorkflowProvider _workflowProvider; 142 /** Workflow helper component */ 143 protected ContentWorkflowHelper _contentWorkflowHelper; 144 /** Component to manager lock */ 145 protected LockContentManager _lockManager; 146 /** Content-type extension point */ 147 protected ContentTypeExtensionPoint _contentTypeEP; 148 /** Content helper */ 149 protected ContentHelper _contentHelper; 150 /** Content types helper */ 151 protected ContentTypesHelper _cTypesHelper; 152 /** Rights manager */ 153 protected RightManager _rightManager; 154 /** Cocoon context */ 155 protected Context _context; 156 /** The user manager */ 157 protected UserManager _usersManager; 158 /** Helper for users */ 159 protected UserHelper _userHelper; 160 /** The helper component for hierarchical simple contents */ 161 protected HierarchicalReferenceTablesHelper _hierarchicalSimpleContentsHelper; 162 /** The modifiable content helper */ 163 protected ModifiableContentHelper _modifiableContentHelper; 164 165 /** The mode for tag edition */ 166 public enum TagMode 167 { 168 /** Value will replace existing one */ 169 REPLACE, 170 /** Value will be added to existing one */ 171 INSERT, 172 /** Value will be removed from existing one */ 173 REMOVE 174 } 175 176 public void contextualize(Context context) throws ContextException 177 { 178 _context = context; 179 } 180 181 @Override 182 public void service(ServiceManager smanager) throws ServiceException 183 { 184 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 185 _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE); 186 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 187 _usersManager = (UserManager) smanager.lookup(UserManager.ROLE); 188 _userHelper = (UserHelper) smanager.lookup(UserHelper.ROLE); 189 _tagProvider = (TagProviderExtensionPoint) smanager.lookup(TagProviderExtensionPoint.ROLE); 190 _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE); 191 _rightManager = (RightManager) smanager.lookup(RightManager.ROLE); 192 _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 193 _lockManager = (LockContentManager) smanager.lookup(LockContentManager.ROLE); 194 _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE); 195 _cTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE); 196 _contentHelper = (ContentHelper) smanager.lookup(ContentHelper.ROLE); 197 _modifiableContentHelper = (ModifiableContentHelper) smanager.lookup(ModifiableContentHelper.ROLE); 198 _hierarchicalSimpleContentsHelper = (HierarchicalReferenceTablesHelper) smanager.lookup(HierarchicalReferenceTablesHelper.ROLE); 199 } 200 201 /** 202 * Delete contents and force the deletion of invert relations. 203 * @param contentsId The ids of contents to delete 204 * @return the deleted and undeleted contents 205 */ 206 @Callable 207 public Map<String, Object> forceDeleteContents(List<String> contentsId) 208 { 209 return forceDeleteContentsObj( 210 contentsId.stream() 211 .map(_resolver::<Content>resolveById) 212 .collect(Collectors.toList()), 213 getRightToDelete() 214 ); 215 } 216 217 /** 218 * Delete contents and force the deletion of invert relations. 219 * @param contents The contents to delete 220 * @param deleteRightId The deletion right's id to check. Can be null to ignore rights 221 * @return the deleted and undeleted contents 222 */ 223 public Map<String, Object> forceDeleteContentsObj(List<Content> contents, String deleteRightId) 224 { 225 Map<String, Object> results = _initializeResultsMap(); 226 227 for (Content content : contents) 228 { 229 Map<String, Object> contentParams = _transformContentToParams(content); 230 231 String contentDeletionStatus = _getContentDeletionStatus(content, deleteRightId); 232 233 // The content is referenced, remove referencies 234 // Then remove referencies if you can 235 // Then check that the content is not referenced any more (should not happen) 236 if (contentDeletionStatus != null && contentDeletionStatus.equals(_CONTENT_DELETION_STATUS_REFERENCED) && _removeReferences(content) && !_isContentReferenced(content)) 237 { 238 contentDeletionStatus = null; 239 } 240 241 // The content has no constraints 242 if (contentDeletionStatus == null) 243 { 244 contentDeletionStatus = _reallyDeleteContent(content); 245 } 246 247 String key = contentDeletionStatus + "-contents"; 248 @SuppressWarnings("unchecked") 249 List<Map<String, Object>> statusedContents = (List<Map<String, Object>>) results.get(key); 250 statusedContents.add(contentParams); 251 } 252 253 return results; 254 } 255 256 /** 257 * Get the invert action id (used for forced deletion). 258 * @return The invert action id 259 */ 260 protected int _getInvertActionId() 261 { 262 return EditContentFunction.INVERT_EDIT_ACTION_ID; 263 } 264 265 /** 266 * Get the right to delete a content. 267 * @return The right ID to delete a content 268 */ 269 protected String getRightToDelete() 270 { 271 return "CMS_Rights_DeleteContent"; 272 } 273 274 /** 275 * Remove all the references to the given content. 276 * @param content The content 277 * @return <code>true</code> if references have been all removed successfully 278 */ 279 protected boolean _removeReferences(Content content) 280 { 281 int actionId = _getInvertActionId(); 282 283 // Group references by referencer 284 Map<Content, Set<String>> referencesByContent = new HashMap<>(); 285 for (Pair<String, Content> referencingPair : _contentHelper.getReferencingContents(content)) 286 { 287 Set<String> references = referencesByContent.computeIfAbsent(referencingPair.getValue(), __ -> new HashSet<>()); 288 references.add(referencingPair.getKey()); 289 } 290 291 // Control that each referencer has the invert action available 292 for (Content referencingContent : referencesByContent.keySet()) 293 { 294 if (referencingContent instanceof WorkflowAwareContent 295 && !_contentWorkflowHelper.isAvailableAction((WorkflowAwareContent) referencingContent, actionId)) 296 { 297 getLogger().warn("The action " + actionId + " is not available for the referencing content " + referencingContent.getId() + ". Impossible to delete the content " + content.getId() + "."); 298 return false; 299 } 300 } 301 302 // Get the references 303 for (Content referencingContent : referencesByContent.keySet()) 304 { 305 // Break the references 306 for (String referencingPath : referencesByContent.get(referencingContent)) 307 { 308 if (referencingContent instanceof ModifiableContent) 309 { 310 if (referencingContent.isMultiple(referencingPath)) 311 { 312 String[] values = ContentDataHelper.getContentIdsStreamFromMultipleContentData(referencingContent, referencingPath) 313 .filter(id -> !id.equals(content.getId())) 314 .toArray(String[]::new); 315 ((ModifiableContent) referencingContent).setValue(referencingPath, values); 316 } 317 else 318 { 319 ((ModifiableContent) referencingContent).removeValue(referencingPath); 320 } 321 } 322 } 323 324 // edit 325 Map<String, Object> contextParameters = new HashMap<>(); 326 contextParameters.put("quit", true); 327 328 Map<String, Object> inputs = new HashMap<>(); 329 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters); 330 inputs.put(CheckRightsCondition.FORCE, true); 331 332 if (referencingContent instanceof WorkflowAwareContent) 333 { 334 try 335 { 336 _contentWorkflowHelper.doAction((WorkflowAwareContent) referencingContent, actionId, inputs); 337 } 338 catch (WorkflowException e) 339 { 340 getLogger().error("An error occured while trying to update the workflow of the referencer " + referencingContent.toString(), e); 341 } 342 } 343 } 344 345 return true; 346 } 347 348 /** 349 * Delete contents 350 * @param contentsId The ids of contents to delete 351 * @return the deleted and undeleted contents 352 */ 353 @Callable 354 public Map<String, Object> deleteContents(List<String> contentsId) 355 { 356 return deleteContents(contentsId, false); 357 } 358 359 /** 360 * Delete contents 361 * @param contentsId The ids of contents to delete 362 * @param ignoreRights true to ignore user rights 363 * @return the deleted and undeleted contents 364 */ 365 public Map<String, Object> deleteContents(List<String> contentsId, boolean ignoreRights) 366 { 367 return deleteContents(contentsId, ignoreRights ? null : getRightToDelete()); 368 } 369 370 /** 371 * Delete contents 372 * @param contentsId The ids of contents to delete 373 * @param deleteRightId The deletion right's id to check. Can be null to ignore rights 374 * @return the deleted and undeleted contents 375 */ 376 public Map<String, Object> deleteContents(List<String> contentsId, String deleteRightId) 377 { 378 Map<String, Object> results = _initializeResultsMap(); 379 380 for (String contentId : contentsId) 381 { 382 Content content = _resolver.resolveById(contentId); 383 Map<String, Object> contentParams = _transformContentToParams(content); 384 385 String contentDeletionStatus = _getContentDeletionStatus(content, deleteRightId); 386 387 // The content has no constraints 388 if (contentDeletionStatus == null) 389 { 390 contentDeletionStatus = _reallyDeleteContent(content); 391 } 392 393 String key = contentDeletionStatus + "-contents"; 394 @SuppressWarnings("unchecked") 395 List<Map<String, Object>> statusedContents = (List<Map<String, Object>>) results.get(key); 396 statusedContents.add(contentParams); 397 } 398 399 return results; 400 } 401 402 /** 403 * Initialize the result map. 404 * @return The empty result map. 405 */ 406 protected Map<String, Object> _initializeResultsMap() 407 { 408 Map<String, Object> results = new HashMap<>(); 409 410 results.put(_CONTENT_DELETION_STATUS_DELETED + "-contents", new ArrayList<Map<String, Object>>()); 411 results.put(_CONTENT_DELETION_STATUS_UNDELETED + "-contents", new ArrayList<Map<String, Object>>()); 412 results.put(_CONTENT_DELETION_STATUS_REFERENCED + "-contents", new ArrayList<Map<String, Object>>()); 413 results.put(_CONTENT_DELETION_STATUS_UNAUTHORIZED + "-contents", new ArrayList<Map<String, Object>>()); 414 results.put(_CONTENT_DELETION_STATUS_LOCKED + "-contents", new ArrayList<Map<String, Object>>()); 415 416 return results; 417 } 418 419 /** 420 * Delete the content and notify observers. 421 * @param content The content to delete 422 * @return The deletion status "deleted" or "undeleted" if an exception occurs 423 */ 424 protected String _reallyDeleteContent(Content content) 425 { 426 try 427 { 428 // All checks have been done, the content can be deleted 429 Map<String, Object> eventParams = _getEventParametersForDeletion(content); 430 431 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParams)); 432 433 RemovableAmetysObject removableContent = (RemovableAmetysObject) content; 434 ModifiableAmetysObject parent = removableContent.getParent(); 435 436 // Remove the content. 437 removableContent.remove(); 438 439 parent.saveChanges(); 440 441 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams)); 442 443 return _CONTENT_DELETION_STATUS_DELETED; 444 } 445 catch (AmetysRepositoryException e) 446 { 447 getLogger().error("Unable to delete content '" + content.getId() + "'", e); 448 449 return _CONTENT_DELETION_STATUS_UNDELETED; 450 } 451 } 452 453 /** 454 * Transform the content to a {@link Map} with id, title and name. 455 * @param content The content to transform 456 * @return A {@link Map} with essentials informations of the content 457 */ 458 protected Map<String, Object> _transformContentToParams(Content content) 459 { 460 String contentName = content.getName(); 461 String contentTitle = StringUtils.defaultString(_contentHelper.getTitle(content), contentName); 462 463 Map<String, Object> contentParams = new HashMap<>(); 464 contentParams.put("id", content.getId()); 465 contentParams.put("title", contentTitle); 466 contentParams.put("name", contentName); 467 468 return contentParams; 469 } 470 471 /** 472 * Get the deletion status of the content : 473 * - unauthorized: The content can't be deleted because of rights 474 * - locked: The content is locked 475 * - referenced: The content has ingoing references 476 * @param content The content 477 * @param deleteRightId The right ID 478 * @return <code>null</code> if content deletion can be done or the status if there is something wrong for deletion 479 */ 480 protected String _getContentDeletionStatus(Content content, String deleteRightId) 481 { 482 if (!(content instanceof RemovableAmetysObject)) 483 { 484 throw new IllegalArgumentException("The content [" + content.getId() + "] is not a RemovableAmetysObject, it can't be deleted."); 485 } 486 487 if (deleteRightId != null && !canDelete(content, deleteRightId)) 488 { 489 // User has no sufficient right 490 return _CONTENT_DELETION_STATUS_UNAUTHORIZED; 491 } 492 493 if (content instanceof LockableAmetysObject) 494 { 495 // If the content is locked, try to unlock it. 496 LockableAmetysObject lockableContent = (LockableAmetysObject) content; 497 if (lockableContent.isLocked()) 498 { 499 boolean canUnlockAll = _rightManager.hasRight(_currentUserProvider.getUser(), "CMS_Rights_UnlockAll", "/cms") == RightResult.RIGHT_ALLOW; 500 if (LockHelper.isLockOwner(lockableContent, _currentUserProvider.getUser()) || canUnlockAll) 501 { 502 lockableContent.unlock(); 503 } 504 else 505 { 506 return _CONTENT_DELETION_STATUS_LOCKED; 507 } 508 } 509 } 510 511 if (_isContentReferenced(content)) 512 { 513 // Indicate that the content is referenced. 514 return _CONTENT_DELETION_STATUS_REFERENCED; 515 } 516 517 return null; 518 } 519 520 /** 521 * Test if content is still referenced before removing it 522 * @param content The content to remove 523 * @return true if content is still referenced 524 */ 525 protected boolean _isContentReferenced (Content content) 526 { 527 return content.hasReferencingContents(); 528 } 529 530 /** 531 * Get parameters for content deleted {@link Event} 532 * @param content the removed content 533 * @return the event's parameters 534 */ 535 protected Map<String, Object> _getEventParametersForDeletion (Content content) 536 { 537 Map<String, Object> eventParams = new HashMap<>(); 538 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 539 eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName()); 540 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 541 return eventParams; 542 } 543 544 /** 545 * Get the contents properties 546 * @param contentIds The ids of contents 547 * @param workspaceName The workspace name. Can be null to get contents in current workspace. 548 * @return The contents' properties 549 */ 550 @Callable 551 public Map<String, Object> getContentsProperties (List<String> contentIds, String workspaceName) 552 { 553 Map<String, Object> result = new HashMap<>(); 554 555 List<Map<String, Object>> contents = new ArrayList<>(); 556 List<String> contentsNotFound = new ArrayList<>(); 557 558 Request request = ContextHelper.getRequest(_context); 559 String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 560 try 561 { 562 if (StringUtils.isNotEmpty(workspaceName)) 563 { 564 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName); 565 } 566 567 for (String contentId : contentIds) 568 { 569 try 570 { 571 Content content = _resolver.resolveById(contentId); 572 contents.add(getContentProperties(content)); 573 } 574 catch (UnknownAmetysObjectException e) 575 { 576 contentsNotFound.add(contentId); 577 } 578 } 579 580 result.put("contents", contents); 581 result.put("contentsNotFound", contentsNotFound); 582 } 583 finally 584 { 585 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace); 586 } 587 588 return result; 589 } 590 591 /** 592 * Get the content properties 593 * @param contentId The id of content 594 * @param workspaceName The workspace name. Can be null to get content in current workspace. 595 * @return The content's properties 596 */ 597 @Callable 598 public Map<String, Object> getContentProperties (String contentId, String workspaceName) 599 { 600 Request request = ContextHelper.getRequest(_context); 601 String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 602 try 603 { 604 if (StringUtils.isNotEmpty(workspaceName)) 605 { 606 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName); 607 } 608 609 Content content = _resolver.resolveById(contentId); 610 return getContentProperties(content); 611 } 612 finally 613 { 614 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace); 615 } 616 } 617 618 /** 619 * Get the content properties 620 * @param content The content 621 * @return The content properties 622 */ 623 public Map<String, Object> getContentProperties (Content content) 624 { 625 Map<String, Object> infos = new HashMap<>(); 626 627 infos.put("id", content.getId()); 628 infos.put("name", content.getName()); 629 infos.put("title", _contentHelper.getTitle(content)); 630 631 if (ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(content.getType(Content.ATTRIBUTE_TITLE).getId())) 632 { 633 infos.put("titleVariants", _contentHelper.getTitleVariants(content)); 634 } 635 infos.put("path", content.getPath()); 636 infos.put("types", content.getTypes()); 637 infos.put("mixins", content.getMixinTypes()); 638 String lang = content.getLanguage(); 639 if (lang != null) 640 { 641 infos.put("lang", lang); 642 } 643 infos.put("creator", _userHelper.user2json(content.getCreator())); 644 infos.put("lastContributor", _userHelper.user2json(content.getLastContributor())); 645 infos.put("creationDate", DateUtils.dateToString(content.getCreationDate())); 646 infos.put("lastModified", DateUtils.dateToString(content.getLastModified())); 647 infos.put("isSimple", _contentHelper.isSimple(content)); 648 infos.put("isReferenceTable", _contentHelper.isReferenceTable(content)); 649 infos.put("parent", _hierarchicalSimpleContentsHelper.getParent(content)); 650 651 if (content instanceof WorkflowAwareContent) 652 { 653 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 654 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 655 656 infos.put("workflowName", workflow.getWorkflowName(waContent.getWorkflowId())); 657 658 List<Integer> workflowSteps = new ArrayList<>(); 659 660 List<Step> currentSteps = workflow.getCurrentSteps(waContent.getWorkflowId()); 661 for (Step step : currentSteps) 662 { 663 workflowSteps.add(step.getStepId()); 664 } 665 infos.put("workflowSteps", workflowSteps); 666 667 int[] availableActions = _contentWorkflowHelper.getAvailableActions(waContent); 668 infos.put("availableActions", availableActions); 669 } 670 671 if (content instanceof ModifiableContent) 672 { 673 infos.put("isModifiable", true); 674 } 675 676 if (content instanceof LockAwareAmetysObject) 677 { 678 LockAwareAmetysObject lockableContent = (LockAwareAmetysObject) content; 679 if (lockableContent.isLocked()) 680 { 681 infos.put("locked", true); 682 infos.put("lockOwner", _userHelper.user2json(lockableContent.getLockOwner())); 683 infos.put("canUnlock", _lockManager.canUnlock(lockableContent)); 684 } 685 } 686 687 infos.put("rights", getUserRights(content)); 688 689 Map<String, Object> additionalData = new HashMap<>(); 690 691 String[] contenttypes = content.getTypes(); 692 for (String cTypeId : contenttypes) 693 { 694 ContentType cType = _contentTypeEP.getExtension(cTypeId); 695 if (cType != null) 696 { 697 additionalData.putAll(cType.getAdditionalData(content)); 698 } 699 } 700 701 if (!additionalData.isEmpty()) 702 { 703 infos.put("additionalData", additionalData); 704 } 705 706 infos.put("isTaggable", content instanceof TaggableAmetysObject); 707 708 if (content instanceof ModifiableDataAwareVersionableAmetysObject) 709 { 710 ModifiableModelLessDataHolder unversionedDataHolder = ((ModifiableDataAwareVersionableAmetysObject) content).getUnversionedDataHolder(); 711 if (unversionedDataHolder.hasNonEmptyValue(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE)) 712 { 713 ZonedDateTime scheduledDate = unversionedDataHolder.getValue(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE); 714 infos.put("scheduledArchivingDate", DateUtils.zonedDateTimeToString(scheduledDate)); 715 } 716 } 717 718 if (content instanceof ReportableObject) 719 { 720 infos.put("reportsCount", ((ReportableObject) content).getReportsCount()); 721 } 722 723 return infos; 724 } 725 726 /** 727 * Get the content's properties for description 728 * @param contentId The id of content 729 * @param workspaceName The workspace name. Can be null to get content in current workspace. 730 * @return The content's properties for description 731 */ 732 @Callable 733 public Map<String, Object> getContentDescription (String contentId, String workspaceName) 734 { 735 Request request = ContextHelper.getRequest(_context); 736 String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 737 try 738 { 739 if (StringUtils.isNotEmpty(workspaceName)) 740 { 741 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName); 742 } 743 744 Content content = _resolver.resolveById(contentId); 745 return getContentDescription(content); 746 } 747 finally 748 { 749 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace); 750 } 751 } 752 753 /** 754 *Get the content's properties for description 755 * @param content The content 756 * @return The content's properties for description 757 */ 758 public Map<String, Object> getContentDescription (Content content) 759 { 760 Map<String, Object> infos = new HashMap<>(); 761 762 infos.put("id", content.getId()); 763 infos.put("name", content.getName()); 764 infos.put("title", _contentHelper.getTitle(content)); 765 infos.put("types", content.getTypes()); 766 infos.put("mixins", content.getMixinTypes()); 767 infos.put("lang", content.getLanguage()); 768 infos.put("creator", _userHelper.user2json(content.getCreator())); 769 infos.put("lastContributor", _userHelper.user2json(content.getLastContributor())); 770 infos.put("lastModified", DateUtils.dateToString(content.getLastModified())); 771 infos.put("iconGlyph", _cTypesHelper.getIconGlyph(content)); 772 infos.put("iconDecorator", _cTypesHelper.getIconDecorator(content)); 773 infos.put("smallIcon", _cTypesHelper.getSmallIcon(content)); 774 infos.put("mediumIcon", _cTypesHelper.getMediumIcon(content)); 775 infos.put("largeIcon", _cTypesHelper.getLargeIcon(content)); 776 777 return infos; 778 } 779 780 /** 781 * Get the metadata sets of a content 782 * @param contentId the content's id 783 * @param edition Set to true to get edition metadata set. False otherwise. 784 * @param includeInternal Set to true to include internal metadata sets. 785 * @return the metadata sets 786 */ 787 @Callable 788 public List<Map<String, Object>> getContentMetadataSets (String contentId, boolean edition, boolean includeInternal) 789 { 790 List<Map<String, Object>> metadataSets = new ArrayList<>(); 791 792 Content content = _resolver.resolveById(contentId); 793 String contentTypeId = _cTypesHelper.getContentTypeIdForRendering(content); 794 795 ContentType cType = _contentTypeEP.getExtension(contentTypeId); 796 797 Set<String> metadataSetNames = edition ? cType.getEditionMetadataSetNames(includeInternal) : cType.getViewMetadataSetNames(includeInternal); 798 for (String metadataSetName : metadataSetNames) 799 { 800 MetadataSet metadataSet = edition ? cType.getMetadataSetForEdition(metadataSetName) : cType.getMetadataSetForView(metadataSetName); 801 802 Map<String, Object> viewInfos = new HashMap<>(); 803 viewInfos.put("name", metadataSetName); 804 viewInfos.put("label", metadataSet.getLabel()); 805 viewInfos.put("description", metadataSet.getDescription()); 806 metadataSets.add(viewInfos); 807 } 808 809 return metadataSets; 810 } 811 812 /** 813 * Get the user rights on content 814 * @param content The content 815 * @return The user's rights 816 */ 817 protected Set<String> getUserRights (Content content) 818 { 819 UserIdentity user = _currentUserProvider.getUser(); 820 return _rightManager.getUserRights(user, content); 821 } 822 823 /** 824 * Get the tags of contents 825 * @param contentIds The content's ids 826 * @return the tags 827 */ 828 @Callable 829 public Set<String> getTags (List<String> contentIds) 830 { 831 Set<String> tags = new HashSet<>(); 832 833 for (String contentId : contentIds) 834 { 835 Content content = _resolver.resolveById(contentId); 836 tags.addAll(content.getTags()); 837 } 838 839 return tags; 840 } 841 842 /** 843 * Tag a list of contents with the given tags 844 * @param contentIds The ids of contents to tag 845 * @param tagNames The tags 846 * @param contextualParameters The contextual parameters 847 * @return the result 848 */ 849 @Callable 850 public Map<String, Object> tag (List<String> contentIds, List<String> tagNames, Map<String, Object> contextualParameters) 851 { 852 return tag(contentIds, tagNames, TagMode.REPLACE.toString(), contextualParameters); 853 } 854 855 /** 856 * Tag a list of contents 857 * @param contentIds The ids of contents to tag 858 * @param tagNames The tags 859 * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags. 860 * @param contextualParameters The contextual parameters 861 * @return the result 862 */ 863 @Callable 864 public Map<String, Object> tag (List<String> contentIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters) 865 { 866 Map<String, Object> result = new HashMap<>(); 867 868 result.put("notaggable-contents", new ArrayList<Map<String, Object>>()); 869 result.put("invalid-tags", new ArrayList<String>()); 870 result.put("allright-contents", new ArrayList<Map<String, Object>>()); 871 result.put("locked-contents", new ArrayList<Map<String, Object>>()); 872 873 for (String contentId : contentIds) 874 { 875 Content content = _resolver.resolveById(contentId); 876 877 Map<String, Object> content2json = new HashMap<>(); 878 content2json.put("id", content.getId()); 879 content2json.put("title", _contentHelper.getTitle(content)); 880 881 if (content instanceof TaggableAmetysObject) 882 { 883 TaggableAmetysObject mContent = (TaggableAmetysObject) content; 884 885 boolean wasLocked = false; 886 887 if (content instanceof LockableAmetysObject) 888 { 889 LockableAmetysObject lockableContent = (LockableAmetysObject) content; 890 UserIdentity user = _currentUserProvider.getUser(); 891 if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, user)) 892 { 893 @SuppressWarnings("unchecked") 894 List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) result.get("locked-contents"); 895 content2json.put("lockOwner", lockableContent.getLockOwner()); 896 lockedContents.add(content2json); 897 898 // Stop process 899 continue; 900 } 901 902 if (lockableContent.isLocked()) 903 { 904 wasLocked = true; 905 lockableContent.unlock(); 906 } 907 } 908 909 TagMode tagMode = TagMode.valueOf(mode); 910 911 Set<String> oldTags = mContent.getTags(); 912 _removeAllTagsInReplaceMode(mContent, tagMode, oldTags); 913 914 // Then set new tags 915 for (String tagName : tagNames) 916 { 917 if (_isTagValid(tagName, contextualParameters)) 918 { 919 if (TagMode.REMOVE.equals(tagMode)) 920 { 921 mContent.untag(tagName); 922 } 923 else if (TagMode.REPLACE.equals(tagMode) || !oldTags.contains(tagName)) 924 { 925 mContent.tag(tagName); 926 } 927 928 } 929 else 930 { 931 @SuppressWarnings("unchecked") 932 List<String> invalidTags = (List<String>) result.get("invalid-tags"); 933 invalidTags.add(tagName); 934 } 935 } 936 937 ((ModifiableAmetysObject) content).saveChanges(); 938 939 if (wasLocked) 940 { 941 // Relock content if it was locked before tagging 942 ((LockableAmetysObject) content).lock(); 943 } 944 945 content2json.put("tags", content.getTags()); 946 @SuppressWarnings("unchecked") 947 List<Map<String, Object>> allRightPages = (List<Map<String, Object>>) result.get("allright-contents"); 948 allRightPages.add(content2json); 949 950 if (!oldTags.equals(content.getTags())) 951 { 952 // Notify observers that the content has been tagged 953 Map<String, Object> eventParams = new HashMap<>(); 954 eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT, content); 955 eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT_ID, content.getId()); 956 eventParams.put("content.tags", content.getTags()); 957 eventParams.put("content.old.tags", oldTags); 958 _observationManager.notify(new Event(org.ametys.cms.ObservationConstants.EVENT_CONTENT_TAGGED, _currentUserProvider.getUser(), eventParams)); 959 } 960 } 961 else 962 { 963 @SuppressWarnings("unchecked") 964 List<Map<String, Object>> notaggableContents = (List<Map<String, Object>>) result.get("notaggable-contents"); 965 notaggableContents.add(content2json); 966 } 967 } 968 969 return result; 970 } 971 972 /** 973 * Remove all tags from the given content if tagMode is equals to REPLACE. 974 * @param mContent The content 975 * @param tagMode The tag 976 * @param oldTags Tags to remove 977 */ 978 protected void _removeAllTagsInReplaceMode(TaggableAmetysObject mContent, TagMode tagMode, Set<String> oldTags) 979 { 980 if (TagMode.REPLACE.equals(tagMode)) 981 { 982 // First delete old tags 983 for (String tagName : oldTags) 984 { 985 mContent.untag(tagName); 986 } 987 } 988 } 989 990 /** 991 * Is the tag a content tag 992 * @param tagName The tag name 993 * @param contextualParameters The contextual parameters 994 * @return true if the tag is a valid content tag 995 */ 996 protected boolean _isTagValid (String tagName, Map<String, Object> contextualParameters) 997 { 998 CMSTag tag = _tagProvider.getTag(tagName, contextualParameters); 999 return tag.getTarget().getName().equals("CONTENT"); 1000 } 1001 1002 /** 1003 * Copy a content. 1004 * @param originalContent the original content. 1005 * @param parent the object in which to create a content. 1006 * @param name the content name. 1007 * @param initWorkflowActionId The initial workflow action id 1008 * @return the copied content. 1009 * @throws AmetysRepositoryException If an error occured 1010 */ 1011 public ModifiableContent copy(DefaultContent originalContent, ModifiableTraversableAmetysObject parent, String name, int initWorkflowActionId) throws AmetysRepositoryException 1012 { 1013 return copy(originalContent, parent, name, null, initWorkflowActionId); 1014 } 1015 1016 /** 1017 * Copy a content. 1018 * @param originalContent the original content. 1019 * @param parent the object in which to create a content. 1020 * @param name the content name. 1021 * @param lang the content language. If null, the content language will be the same of the original content 1022 * @param initWorkflowActionId The initial workflow action id 1023 * @return the copied content. 1024 * @throws AmetysRepositoryException If an error occured 1025 */ 1026 public ModifiableContent copy(DefaultContent originalContent, ModifiableTraversableAmetysObject parent, String name, String lang, int initWorkflowActionId) throws AmetysRepositoryException 1027 { 1028 return copy(originalContent, parent, name, lang, initWorkflowActionId, true, false, false); 1029 } 1030 1031 /** 1032 * Copy a content. 1033 * @param originalContent the original content. 1034 * @param parent the object in which to create a content. 1035 * @param name the content name. 1036 * @param lang the content language. If null, the content language will be the same of the original content 1037 * @param initWorkflowActionId The initial workflow action id 1038 * @param notifyObservers Set to false to do not fire observer events 1039 * @param waitAsyncObservers true to wait for asynchronous observers to complete 1040 * @param copyACL true to copy ACL of source content 1041 * @return the copied content. 1042 * @throws AmetysRepositoryException If an error occured 1043 */ 1044 public ModifiableContent copy(DefaultContent originalContent, ModifiableTraversableAmetysObject parent, String name, String lang, int initWorkflowActionId, boolean notifyObservers, boolean waitAsyncObservers, boolean copyACL) throws AmetysRepositoryException 1045 { 1046 try 1047 { 1048 String originalName = name == null ? originalContent.getName() : name; 1049 String contentName = originalName; 1050 int index = 2; 1051 while (parent.hasChild(contentName)) 1052 { 1053 contentName = originalName + "-" + (index++); 1054 } 1055 1056 String originalContentType = originalContent.getNode().getPrimaryNodeType().getName(); 1057 1058 ModifiableContent content = parent.createChild(contentName, originalContentType); 1059 1060 String targetLanguage = lang == null ? originalContent.getLanguage() : lang; 1061 if (targetLanguage != null) 1062 { 1063 content.setLanguage(targetLanguage); 1064 } 1065 1066 content.setTypes(originalContent.getTypes()); 1067 1068 _modifiableContentHelper.copyTitle(originalContent, content); 1069 1070 if (originalContent instanceof WorkflowAwareContent) 1071 { 1072 WorkflowAwareContent waOriginalContent = (WorkflowAwareContent) originalContent; 1073 AmetysObjectWorkflow originalContentWorkflow = _workflowProvider.getAmetysObjectWorkflow(waOriginalContent); 1074 String workflowName = originalContentWorkflow.getWorkflowName(waOriginalContent.getWorkflowId()); 1075 1076 // Initialize new content workflow 1077 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 1078 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 1079 1080 HashMap<String, Object> inputs = new HashMap<>(); 1081 // Provide the content key 1082 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, waContent); 1083 1084 long workflowId = workflow.initialize(workflowName, initWorkflowActionId, inputs); 1085 waContent.setWorkflowId(workflowId); 1086 1087 // Set the current step ID property 1088 Step currentStep = (Step) workflow.getCurrentSteps(workflowId).iterator().next(); 1089 waContent.setCurrentStepId(currentStep.getStepId()); 1090 1091 Node workflowEntryNode = null; 1092 Node node = waContent.getNode(); 1093 Session session = node.getSession(); 1094 1095 1096 try 1097 { 1098 AbstractJackrabbitWorkflowStore workflowStore = (AbstractJackrabbitWorkflowStore) workflow.getConfiguration().getWorkflowStore(); 1099 1100 if (workflowStore instanceof AmetysObjectWorkflowStore) 1101 { 1102 AmetysObjectWorkflowStore ametysObjectWorkflowStore = (AmetysObjectWorkflowStore) workflowStore; 1103 ametysObjectWorkflowStore.bindAmetysObject(waContent); 1104 } 1105 1106 workflowEntryNode = workflowStore.getEntryNode(session, workflowId); 1107 workflowEntryNode.setProperty("ametys-internal:initialActionId", initWorkflowActionId); 1108 } 1109 catch (RepositoryException e) 1110 { 1111 throw new AmetysRepositoryException("Unable to link the workflow to the content", e); 1112 } 1113 } 1114 1115 // Copy metadata 1116 originalContent.getMetadataHolder().copyTo(content.getMetadataHolder()); 1117 1118 // Copy attachments 1119 _copyAttachments(originalContent, content); 1120 1121 if (copyACL) 1122 { 1123 _copyACL(originalContent, content); 1124 } 1125 1126 if (_currentUserProvider.getUser() != null) 1127 { 1128 content.setCreator(_currentUserProvider.getUser()); 1129 content.setLastModified(new Date()); 1130 content.setCreationDate(new Date()); 1131 } 1132 1133 parent.saveChanges(); 1134 1135 // Create a new version 1136 if (content instanceof VersionableAmetysObject) 1137 { 1138 ((VersionableAmetysObject) content).checkpoint(); 1139 } 1140 1141 if (notifyObservers) 1142 { 1143 _notifyContentCopied(content, waitAsyncObservers); 1144 } 1145 1146 return content; 1147 } 1148 catch (WorkflowException e) 1149 { 1150 throw new AmetysRepositoryException(e); 1151 } 1152 catch (RepositoryException e) 1153 { 1154 throw new AmetysRepositoryException(e); 1155 } 1156 } 1157 1158 /** 1159 * Copy the attachments of a content 1160 * @param srcContent The source content 1161 * @param targetContent The target content 1162 */ 1163 protected void _copyAttachments(Content srcContent, Content targetContent) 1164 { 1165 ResourceCollection srcRootAttachments = srcContent.getRootAttachments(); 1166 ResourceCollection targetRootAttachments = targetContent.getRootAttachments(); 1167 1168 AmetysObjectIterable<AmetysObject> children = srcRootAttachments.getChildren(); 1169 for (AmetysObject child : children) 1170 { 1171 if (child instanceof CopiableAmetysObject) 1172 { 1173 try 1174 { 1175 ((CopiableAmetysObject) child).copyTo((ModifiableTraversableAmetysObject) targetRootAttachments, child.getName()); 1176 } 1177 catch (AmetysRepositoryException e) 1178 { 1179 getLogger().error("Failed to copy attachments at path " + child.getPath() + " from content " + srcContent + " to content " + targetContent, e); 1180 } 1181 } 1182 } 1183 } 1184 1185 /** 1186 * Copy the ACL of a content 1187 * @param srcContent The source content 1188 * @param targetContent The target content 1189 */ 1190 protected void _copyACL(Content srcContent, Content targetContent) 1191 { 1192 if (srcContent instanceof DefaultContent && targetContent instanceof DefaultContent) 1193 { 1194 Node srcNode = ((DefaultContent) srcContent).getNode(); 1195 Node targetNode = ((DefaultContent) targetContent).getNode(); 1196 1197 try 1198 { 1199 String aclNodeName = "ametys-internal:acl"; 1200 if (srcNode.hasNode(aclNodeName)) 1201 { 1202 Node aclNode = srcNode.getNode(aclNodeName); 1203 aclNode.getSession().getWorkspace().copy(aclNode.getPath(), targetNode.getPath() + "/" + aclNodeName); 1204 } 1205 } 1206 catch (RepositoryException e) 1207 { 1208 getLogger().error("Failed to copy ACL from content " + srcContent + " to content " + targetContent, e); 1209 } 1210 } 1211 } 1212 1213 /** 1214 * Notify observers that the content has been created 1215 * @param content The content added 1216 * @param waitAsyncObservers true to wait for asynchonous observers to finish 1217 * @throws WorkflowException If an error occurred 1218 */ 1219 protected void _notifyContentCopied(Content content, boolean waitAsyncObservers) throws WorkflowException 1220 { 1221 Map<String, Object> eventParams = new HashMap<>(); 1222 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 1223 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 1224 1225 List<Future> futures = _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_ADDED, _currentUserProvider.getUser(), eventParams)); 1226 futures.addAll(_observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_WORKFLOW_CHANGED, _currentUserProvider.getUser(), eventParams))); 1227 1228 if (waitAsyncObservers) 1229 { 1230 // Wait for asynchonous observers to finish 1231 for (Future future : futures) 1232 { 1233 try 1234 { 1235 future.get(); 1236 } 1237 catch (ExecutionException | InterruptedException e) 1238 { 1239 getLogger().error(String.format("Error while waiting for async observer to complete")); 1240 } 1241 } 1242 } 1243 } 1244 1245 /** 1246 * Returns the content's attachments root node 1247 * @param id the content's id 1248 * @return The attachments' root node informations 1249 */ 1250 @Callable 1251 public Map<String, Object> getAttachmentsRootNode (String id) 1252 { 1253 Map<String, Object> result = new HashMap<>(); 1254 1255 Content content = _resolver.resolveById(id); 1256 1257 result.put("title", _contentHelper.getTitle(content)); 1258 result.put("contentId", content.getId()); 1259 1260 TraversableAmetysObject attachments = content.getRootAttachments(); 1261 1262 if (attachments != null) 1263 { 1264 result.put("id", attachments.getId()); 1265 if (attachments instanceof ModifiableAmetysObject) 1266 { 1267 result.put("isModifiable", true); 1268 } 1269 if (attachments instanceof ModifiableResourceCollection) 1270 { 1271 result.put("canCreateChild", true); 1272 } 1273 1274 boolean hasChildNodes = false; 1275 boolean hasResources = false; 1276 1277 for (AmetysObject child : attachments.getChildren()) 1278 { 1279 if (child instanceof Resource) 1280 { 1281 hasResources = true; 1282 } 1283 else if (child instanceof ExplorerNode) 1284 { 1285 hasChildNodes = true; 1286 } 1287 } 1288 1289 if (hasChildNodes) 1290 { 1291 result.put("hasChildNodes", true); 1292 } 1293 1294 if (hasResources) 1295 { 1296 result.put("hasResources", true); 1297 } 1298 1299 return result; 1300 } 1301 1302 throw new IllegalArgumentException("Content with id '" + id + "' does not support attachments."); 1303 } 1304 1305 /** 1306 * Determines if the current user has right to delete the content 1307 * @param content The content 1308 * @return true if current user is authorized to delete the content 1309 */ 1310 public boolean canDelete(Content content) 1311 { 1312 return canDelete(content, getRightToDelete()); 1313 } 1314 1315 /** 1316 * Determines if the current user has right to delete the content 1317 * @param content The content 1318 * @param deleteRightId The right's id to check for deletion 1319 * @return true if current user is authorized to delete the content 1320 */ 1321 public boolean canDelete(Content content, String deleteRightId) 1322 { 1323 UserIdentity user = _currentUserProvider.getUser(); 1324 if (_rightManager.hasRight(user, deleteRightId, content) == RightResult.RIGHT_ALLOW) 1325 { 1326 return true; 1327 } 1328 1329 return false; 1330 } 1331 1332 /** 1333 * Add or remove a reaction on a content 1334 * @param contentId The content id 1335 * @param reactionName the reaction name (ex: LIKE) 1336 * @param remove true to remove the reaction, false to add reaction 1337 * @return the result with the current actors of this reaction 1338 */ 1339 @Callable 1340 public Map<String, Object> react(String contentId, String reactionName, boolean remove) 1341 { 1342 Map<String, Object> result = new HashMap<>(); 1343 1344 Content content = _resolver.resolveById(contentId); 1345 1346 if (_rightManager.currentUserHasReadAccess(content)) 1347 { 1348 ReactionType reactionType = ReactionType.valueOf(reactionName); 1349 UserIdentity actor = _currentUserProvider.getUser(); 1350 1351 boolean updated = remove ? unreact(content, actor, reactionType) : react(content, actor, reactionType); 1352 result.put("updated", updated); 1353 result.put("contentId", contentId); 1354 result.put("actors", _userHelper.userIdentities2json(((ReactionableObject) content).getReactionUsers(reactionType))); 1355 } 1356 else 1357 { 1358 result.put("unauthorized", true); 1359 result.put("updated", false); 1360 } 1361 1362 return result; 1363 } 1364 1365 /** 1366 * Add a reaction on a {@link Content}. 1367 * @param content the content 1368 * @param userIdentity the issuer of reaction 1369 * @param reactionType the reaction type 1370 * @return true if a change was made 1371 */ 1372 public boolean react(Content content, UserIdentity userIdentity, ReactionType reactionType) 1373 { 1374 return _addOrRemoveReaction(content, userIdentity, reactionType, false); 1375 } 1376 1377 /** 1378 * Remove reaction if exists on a {@link Content}. 1379 * @param content the content 1380 * @param userIdentity the issuer of reaction 1381 * @param reactionType the reaction type 1382 * @return <code>true</code> if a change was made 1383 */ 1384 public boolean unreact(Content content, UserIdentity userIdentity, ReactionType reactionType) 1385 { 1386 return _addOrRemoveReaction(content, userIdentity, reactionType, true); 1387 } 1388 1389 /** 1390 * Add or remove reaction if exists on the given content. 1391 * @param content the content 1392 * @param userIdentity the issuer of reaction 1393 * @param reactionType the reaction type 1394 * @param remove <code>true</code> if it's to remove the reaction 1395 * @return <code>true</code> if a change was made 1396 */ 1397 protected boolean _addOrRemoveReaction(Content content, UserIdentity userIdentity, ReactionType reactionType, boolean remove) 1398 { 1399 if (content instanceof ReactionableObject) 1400 { 1401 boolean hasChanges = false; 1402 1403 List<UserIdentity> reactionIssuers = ((ReactionableObject) content).getReactionUsers(reactionType); 1404 if (!remove && !reactionIssuers.contains(userIdentity)) 1405 { 1406 ((ReactionableObject) content).addReaction(userIdentity, reactionType); 1407 hasChanges = true; 1408 } 1409 else if (remove && reactionIssuers.contains(userIdentity)) 1410 { 1411 ((ReactionableObject) content).removeReaction(userIdentity, reactionType); 1412 hasChanges = true; 1413 } 1414 1415 if (hasChanges) 1416 { 1417 ((DefaultContent) content).saveChanges(); 1418 1419 Map<String, Object> eventParams = new HashMap<>(); 1420 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 1421 eventParams.put(ObservationConstants.ARGS_REACTION_TYPE, reactionType); 1422 eventParams.put(ObservationConstants.ARGS_REACTION_ISSUER, userIdentity); 1423 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_REACTION_CHANGED, userIdentity, eventParams)); 1424 1425 return true; 1426 } 1427 } 1428 1429 return false; 1430 } 1431 1432 /** 1433 * Add a report on a content 1434 * @param content The content 1435 * @throws IllegalArgumentException if the content is not a {@link ReportableObject} 1436 * @throws AccessDeniedException if the current user has not read access on the given content 1437 */ 1438 public void report(Content content) throws IllegalArgumentException, AccessDeniedException 1439 { 1440 if (_rightManager.currentUserHasReadAccess(content)) 1441 { 1442 if (content instanceof ReportableObject) 1443 { 1444 ((ReportableObject) content).addReport(); 1445 ((DefaultContent) content).saveChanges(); 1446 } 1447 else 1448 { 1449 throw new IllegalArgumentException("Unable to report the content '" + content.getId() + "'. Current user is not authorized to see this content."); 1450 } 1451 } 1452 else 1453 { 1454 throw new AccessDeniedException("Unable to report the content '" + content.getId() + "'. Current user is not authorized to see this content."); 1455 } 1456 } 1457 1458 /** 1459 * Delete contents and force the deletion of invert relations, then log the result. 1460 * @param contents The contents to delete 1461 * @param deleteRightId The deletion right's id to check. Can be null to ignore rights 1462 * @param logger The logger 1463 * @return the number of deleted contents 1464 */ 1465 public int forceDeleteContentsWithLog(List<Content> contents, String deleteRightId, Logger logger) 1466 { 1467 return _logResult(forceDeleteContentsObj(contents, deleteRightId), logger); 1468 } 1469 1470 @SuppressWarnings("unchecked") 1471 private int _logResult(Map<String, Object> result, Logger logger) 1472 { 1473 List<Map<String, Object>> referencedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_REFERENCED + "-contents"); 1474 if (referencedContents.size() > 0) 1475 { 1476 logger.info("The following contents cannot be deleted because they are referenced: {}", referencedContents.stream().map(m -> m.get("id")).collect(Collectors.toList())); 1477 } 1478 1479 List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_LOCKED + "-contents"); 1480 if (lockedContents.size() > 0) 1481 { 1482 logger.info("The following contents cannot be deleted because they are locked: {}", lockedContents.stream().map(m -> m.get("id")).collect(Collectors.toList())); 1483 } 1484 1485 List<Map<String, Object>> unauthorizedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_UNAUTHORIZED + "-contents"); 1486 if (unauthorizedContents.size() > 0) 1487 { 1488 logger.info("The following contents cannot be deleted because they are no authorization: {}", unauthorizedContents.stream().map(m -> m.get("id")).collect(Collectors.toList())); 1489 } 1490 1491 List<Map<String, Object>> undeletedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_UNDELETED + "-contents"); 1492 if (undeletedContents.size() > 0) 1493 { 1494 logger.info("{} contents were not deleted. See previous logs for more information.", undeletedContents.size()); 1495 } 1496 1497 List<Map<String, Object>> deletedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_DELETED + "-contents"); 1498 return deletedContents.size(); 1499 } 1500}