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.HashMap; 021import java.util.HashSet; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025import java.util.concurrent.ExecutionException; 026import java.util.concurrent.Future; 027import java.util.stream.Collectors; 028 029import javax.jcr.Node; 030import javax.jcr.RepositoryException; 031import javax.jcr.Session; 032 033import org.apache.avalon.framework.component.Component; 034import org.apache.avalon.framework.context.Context; 035import org.apache.avalon.framework.context.ContextException; 036import org.apache.avalon.framework.context.Contextualizable; 037import org.apache.avalon.framework.logger.AbstractLogEnabled; 038import org.apache.avalon.framework.service.ServiceException; 039import org.apache.avalon.framework.service.ServiceManager; 040import org.apache.avalon.framework.service.Serviceable; 041import org.apache.cocoon.components.ContextHelper; 042import org.apache.cocoon.environment.Request; 043import org.apache.commons.lang3.StringUtils; 044import org.apache.commons.lang3.tuple.Pair; 045import org.slf4j.Logger; 046 047import org.ametys.cms.ObservationConstants; 048import org.ametys.cms.content.ContentHelper; 049import org.ametys.cms.content.archive.ArchiveConstants; 050import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper; 051import org.ametys.cms.contenttype.ContentType; 052import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 053import org.ametys.cms.contenttype.ContentTypesHelper; 054import org.ametys.cms.data.ContentDataHelper; 055import org.ametys.cms.data.type.ModelItemTypeConstants; 056import org.ametys.cms.lock.LockContentManager; 057import org.ametys.cms.repository.ReactionableObject.ReactionType; 058import org.ametys.cms.tag.CMSTag; 059import org.ametys.cms.tag.TagProviderExtensionPoint; 060import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 061import org.ametys.cms.workflow.ContentWorkflowHelper; 062import org.ametys.cms.workflow.EditContentFunction; 063import org.ametys.core.observation.Event; 064import org.ametys.core.observation.ObservationManager; 065import org.ametys.core.right.RightManager; 066import org.ametys.core.right.RightManager.RightResult; 067import org.ametys.core.ui.Callable; 068import org.ametys.core.user.CurrentUserProvider; 069import org.ametys.core.user.UserIdentity; 070import org.ametys.core.user.UserManager; 071import org.ametys.core.util.DateUtils; 072import org.ametys.plugins.core.user.UserHelper; 073import org.ametys.plugins.explorer.ExplorerNode; 074import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 075import org.ametys.plugins.explorer.resources.Resource; 076import org.ametys.plugins.explorer.resources.ResourceCollection; 077import org.ametys.plugins.repository.AmetysObject; 078import org.ametys.plugins.repository.AmetysObjectIterable; 079import org.ametys.plugins.repository.AmetysObjectResolver; 080import org.ametys.plugins.repository.AmetysRepositoryException; 081import org.ametys.plugins.repository.CopiableAmetysObject; 082import org.ametys.plugins.repository.ModifiableAmetysObject; 083import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 084import org.ametys.plugins.repository.RemovableAmetysObject; 085import org.ametys.plugins.repository.TraversableAmetysObject; 086import org.ametys.plugins.repository.UnknownAmetysObjectException; 087import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder; 088import org.ametys.plugins.repository.lock.LockAwareAmetysObject; 089import org.ametys.plugins.repository.lock.LockHelper; 090import org.ametys.plugins.repository.lock.LockableAmetysObject; 091import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 092import org.ametys.plugins.repository.tag.TaggableAmetysObject; 093import org.ametys.plugins.repository.version.ModifiableDataAwareVersionableAmetysObject; 094import org.ametys.plugins.repository.version.VersionableAmetysObject; 095import org.ametys.plugins.workflow.AbstractWorkflowComponent; 096import org.ametys.plugins.workflow.component.CheckRightsCondition; 097import org.ametys.plugins.workflow.store.AbstractJackrabbitWorkflowStore; 098import org.ametys.plugins.workflow.store.AmetysObjectWorkflowStore; 099import org.ametys.plugins.workflow.support.WorkflowProvider; 100import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 101import org.ametys.runtime.authentication.AccessDeniedException; 102import org.ametys.runtime.model.View; 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.zonedDateTimeToString(content.getCreationDate())); 646 infos.put("lastModified", DateUtils.zonedDateTimeToString(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.hasValue(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.zonedDateTimeToString(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 views of a content 782 * @param contentId the content's id 783 * @param includeInternal Set to true to include internal views. 784 * @return the views 785 */ 786 @Callable 787 public List<Map<String, Object>> getContentViews(String contentId, boolean includeInternal) 788 { 789 List<Map<String, Object>> views = new ArrayList<>(); 790 791 Content content = _resolver.resolveById(contentId); 792 String contentTypeId = _cTypesHelper.getContentTypeIdForRendering(content); 793 794 ContentType cType = _contentTypeEP.getExtension(contentTypeId); 795 796 Set<String> viewNames = cType.getViewNames(includeInternal); 797 for (String viewName : viewNames) 798 { 799 View view = cType.getView(viewName); 800 801 Map<String, Object> viewInfos = new HashMap<>(); 802 viewInfos.put("name", viewName); 803 viewInfos.put("label", view.getLabel()); 804 viewInfos.put("description", view.getDescription()); 805 views.add(viewInfos); 806 } 807 808 return views; 809 } 810 811 /** 812 * Get the user rights on content 813 * @param content The content 814 * @return The user's rights 815 */ 816 protected Set<String> getUserRights (Content content) 817 { 818 UserIdentity user = _currentUserProvider.getUser(); 819 return _rightManager.getUserRights(user, content); 820 } 821 822 /** 823 * Get the tags of contents 824 * @param contentIds The content's ids 825 * @return the tags 826 */ 827 @Callable 828 public Set<String> getTags (List<String> contentIds) 829 { 830 Set<String> tags = new HashSet<>(); 831 832 for (String contentId : contentIds) 833 { 834 Content content = _resolver.resolveById(contentId); 835 tags.addAll(content.getTags()); 836 } 837 838 return tags; 839 } 840 841 /** 842 * Tag a list of contents with the given tags 843 * @param contentIds The ids of contents to tag 844 * @param tagNames The tags 845 * @param contextualParameters The contextual parameters 846 * @return the result 847 */ 848 @Callable 849 public Map<String, Object> tag (List<String> contentIds, List<String> tagNames, Map<String, Object> contextualParameters) 850 { 851 return tag(contentIds, tagNames, TagMode.REPLACE.toString(), contextualParameters); 852 } 853 854 /** 855 * Tag a list of contents 856 * @param contentIds The ids of contents to tag 857 * @param tagNames The tags 858 * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags. 859 * @param contextualParameters The contextual parameters 860 * @return the result 861 */ 862 @Callable 863 public Map<String, Object> tag (List<String> contentIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters) 864 { 865 Map<String, Object> result = new HashMap<>(); 866 867 result.put("notaggable-contents", new ArrayList<Map<String, Object>>()); 868 result.put("invalid-tags", new ArrayList<String>()); 869 result.put("allright-contents", new ArrayList<Map<String, Object>>()); 870 result.put("locked-contents", new ArrayList<Map<String, Object>>()); 871 872 for (String contentId : contentIds) 873 { 874 Content content = _resolver.resolveById(contentId); 875 876 Map<String, Object> content2json = new HashMap<>(); 877 content2json.put("id", content.getId()); 878 content2json.put("title", _contentHelper.getTitle(content)); 879 880 if (content instanceof TaggableAmetysObject) 881 { 882 TaggableAmetysObject mContent = (TaggableAmetysObject) content; 883 884 boolean wasLocked = false; 885 886 if (content instanceof LockableAmetysObject) 887 { 888 LockableAmetysObject lockableContent = (LockableAmetysObject) content; 889 UserIdentity user = _currentUserProvider.getUser(); 890 if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, user)) 891 { 892 @SuppressWarnings("unchecked") 893 List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) result.get("locked-contents"); 894 content2json.put("lockOwner", lockableContent.getLockOwner()); 895 lockedContents.add(content2json); 896 897 // Stop process 898 continue; 899 } 900 901 if (lockableContent.isLocked()) 902 { 903 wasLocked = true; 904 lockableContent.unlock(); 905 } 906 } 907 908 TagMode tagMode = TagMode.valueOf(mode); 909 910 Set<String> oldTags = mContent.getTags(); 911 _removeAllTagsInReplaceMode(mContent, tagMode, oldTags); 912 913 // Then set new tags 914 for (String tagName : tagNames) 915 { 916 if (_isTagValid(tagName, contextualParameters)) 917 { 918 if (TagMode.REMOVE.equals(tagMode)) 919 { 920 mContent.untag(tagName); 921 } 922 else if (TagMode.REPLACE.equals(tagMode) || !oldTags.contains(tagName)) 923 { 924 mContent.tag(tagName); 925 } 926 927 } 928 else 929 { 930 @SuppressWarnings("unchecked") 931 List<String> invalidTags = (List<String>) result.get("invalid-tags"); 932 invalidTags.add(tagName); 933 } 934 } 935 936 ((ModifiableAmetysObject) content).saveChanges(); 937 938 if (wasLocked) 939 { 940 // Relock content if it was locked before tagging 941 ((LockableAmetysObject) content).lock(); 942 } 943 944 content2json.put("tags", content.getTags()); 945 @SuppressWarnings("unchecked") 946 List<Map<String, Object>> allRightPages = (List<Map<String, Object>>) result.get("allright-contents"); 947 allRightPages.add(content2json); 948 949 if (!oldTags.equals(content.getTags())) 950 { 951 // Notify observers that the content has been tagged 952 Map<String, Object> eventParams = new HashMap<>(); 953 eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT, content); 954 eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT_ID, content.getId()); 955 eventParams.put("content.tags", content.getTags()); 956 eventParams.put("content.old.tags", oldTags); 957 _observationManager.notify(new Event(org.ametys.cms.ObservationConstants.EVENT_CONTENT_TAGGED, _currentUserProvider.getUser(), eventParams)); 958 } 959 } 960 else 961 { 962 @SuppressWarnings("unchecked") 963 List<Map<String, Object>> notaggableContents = (List<Map<String, Object>>) result.get("notaggable-contents"); 964 notaggableContents.add(content2json); 965 } 966 } 967 968 return result; 969 } 970 971 /** 972 * Remove all tags from the given content if tagMode is equals to REPLACE. 973 * @param mContent The content 974 * @param tagMode The tag 975 * @param oldTags Tags to remove 976 */ 977 protected void _removeAllTagsInReplaceMode(TaggableAmetysObject mContent, TagMode tagMode, Set<String> oldTags) 978 { 979 if (TagMode.REPLACE.equals(tagMode)) 980 { 981 // First delete old tags 982 for (String tagName : oldTags) 983 { 984 mContent.untag(tagName); 985 } 986 } 987 } 988 989 /** 990 * Is the tag a content tag 991 * @param tagName The tag name 992 * @param contextualParameters The contextual parameters 993 * @return true if the tag is a valid content tag 994 */ 995 protected boolean _isTagValid (String tagName, Map<String, Object> contextualParameters) 996 { 997 CMSTag tag = _tagProvider.getTag(tagName, contextualParameters); 998 return tag.getTarget().getName().equals("CONTENT"); 999 } 1000 1001 /** 1002 * Copy a content. 1003 * @param originalContent the original content. 1004 * @param parent the object in which to create a content. 1005 * @param name the content name. 1006 * @param initWorkflowActionId The initial workflow action id 1007 * @return the copied content. 1008 * @throws AmetysRepositoryException If an error occured 1009 */ 1010 public ModifiableContent copy(DefaultContent originalContent, ModifiableTraversableAmetysObject parent, String name, int initWorkflowActionId) throws AmetysRepositoryException 1011 { 1012 return copy(originalContent, parent, name, null, initWorkflowActionId); 1013 } 1014 1015 /** 1016 * Copy a content. 1017 * @param originalContent the original content. 1018 * @param parent the object in which to create a content. 1019 * @param name the content name. 1020 * @param lang the content language. If null, the content language will be the same of the original content 1021 * @param initWorkflowActionId The initial workflow action id 1022 * @return the copied content. 1023 * @throws AmetysRepositoryException If an error occured 1024 */ 1025 public ModifiableContent copy(DefaultContent originalContent, ModifiableTraversableAmetysObject parent, String name, String lang, int initWorkflowActionId) throws AmetysRepositoryException 1026 { 1027 return copy(originalContent, parent, name, lang, initWorkflowActionId, true, true, false, false); 1028 } 1029 1030 /** 1031 * Copy a content. 1032 * @param originalContent the original content. 1033 * @param parent the object in which to create a content. 1034 * @param name the content name. 1035 * @param lang the content language. If null, the content language will be the same of the original content 1036 * @param initWorkflowActionId The initial workflow action id 1037 * @param notifyObservers Set to false to do not fire observer events 1038 * @param checkpoint true to check the content in if it is versionable 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 checkpoint, 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 attributes 1116 originalContent.copyTo(content); 1117 1118 // Copy attachments 1119 _copyAttachments(originalContent, content); 1120 1121 if (copyACL) 1122 { 1123 _copyACL(originalContent, content); 1124 } 1125 1126 UserIdentity currentUser = _currentUserProvider.getUser(); 1127 if (currentUser != null) 1128 { 1129 content.setCreator(currentUser); 1130 content.setLastContributor(currentUser); 1131 content.setLastModified(ZonedDateTime.now()); 1132 content.setCreationDate(ZonedDateTime.now()); 1133 } 1134 1135 parent.saveChanges(); 1136 1137 // Create a new version 1138 if (checkpoint && content instanceof VersionableAmetysObject versionableContent) 1139 { 1140 versionableContent.checkpoint(); 1141 } 1142 1143 if (notifyObservers) 1144 { 1145 notifyContentCopied(content, waitAsyncObservers); 1146 } 1147 1148 return content; 1149 } 1150 catch (WorkflowException e) 1151 { 1152 throw new AmetysRepositoryException(e); 1153 } 1154 catch (RepositoryException e) 1155 { 1156 throw new AmetysRepositoryException(e); 1157 } 1158 } 1159 1160 /** 1161 * Copy the attachments of a content 1162 * @param srcContent The source content 1163 * @param targetContent The target content 1164 */ 1165 protected void _copyAttachments(Content srcContent, Content targetContent) 1166 { 1167 ResourceCollection srcRootAttachments = srcContent.getRootAttachments(); 1168 if (srcRootAttachments == null) 1169 { 1170 // There are no attachments to copy 1171 return; 1172 } 1173 1174 ResourceCollection targetRootAttachments = targetContent.getRootAttachments(); 1175 if (targetRootAttachments == null) 1176 { 1177 // The target is an (unmodifiable) old version and the attachments root is missing 1178 return; 1179 } 1180 1181 AmetysObjectIterable<AmetysObject> children = srcRootAttachments.getChildren(); 1182 for (AmetysObject child : children) 1183 { 1184 if (child instanceof CopiableAmetysObject) 1185 { 1186 try 1187 { 1188 ((CopiableAmetysObject) child).copyTo((ModifiableTraversableAmetysObject) targetRootAttachments, child.getName()); 1189 } 1190 catch (AmetysRepositoryException e) 1191 { 1192 getLogger().error("Failed to copy attachments at path " + child.getPath() + " from content " + srcContent + " to content " + targetContent, e); 1193 } 1194 } 1195 } 1196 } 1197 1198 /** 1199 * Copy the ACL of a content 1200 * @param srcContent The source content 1201 * @param targetContent The target content 1202 */ 1203 protected void _copyACL(Content srcContent, Content targetContent) 1204 { 1205 if (srcContent instanceof DefaultContent && targetContent instanceof DefaultContent) 1206 { 1207 Node srcNode = ((DefaultContent) srcContent).getNode(); 1208 Node targetNode = ((DefaultContent) targetContent).getNode(); 1209 1210 try 1211 { 1212 String aclNodeName = "ametys-internal:acl"; 1213 if (srcNode.hasNode(aclNodeName)) 1214 { 1215 Node aclNode = srcNode.getNode(aclNodeName); 1216 aclNode.getSession().getWorkspace().copy(aclNode.getPath(), targetNode.getPath() + "/" + aclNodeName); 1217 } 1218 } 1219 catch (RepositoryException e) 1220 { 1221 getLogger().error("Failed to copy ACL from content " + srcContent + " to content " + targetContent, e); 1222 } 1223 } 1224 } 1225 1226 /** 1227 * Notify observers that the content has been created 1228 * @param content The content added 1229 * @param waitAsyncObservers true to wait for asynchonous observers to finish 1230 * @throws WorkflowException If an error occurred 1231 */ 1232 public void notifyContentCopied(Content content, boolean waitAsyncObservers) throws WorkflowException 1233 { 1234 Map<String, Object> eventParams = new HashMap<>(); 1235 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 1236 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 1237 1238 List<Future> futures = _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_ADDED, _currentUserProvider.getUser(), eventParams)); 1239 futures.addAll(_observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_WORKFLOW_CHANGED, _currentUserProvider.getUser(), eventParams))); 1240 1241 if (waitAsyncObservers) 1242 { 1243 // Wait for asynchonous observers to finish 1244 for (Future future : futures) 1245 { 1246 try 1247 { 1248 future.get(); 1249 } 1250 catch (ExecutionException | InterruptedException e) 1251 { 1252 getLogger().error(String.format("Error while waiting for async observer to complete")); 1253 } 1254 } 1255 } 1256 } 1257 1258 /** 1259 * Returns the content's attachments root node 1260 * @param id the content's id 1261 * @return The attachments' root node informations 1262 */ 1263 @Callable 1264 public Map<String, Object> getAttachmentsRootNode (String id) 1265 { 1266 Map<String, Object> result = new HashMap<>(); 1267 1268 Content content = _resolver.resolveById(id); 1269 1270 result.put("title", _contentHelper.getTitle(content)); 1271 result.put("contentId", content.getId()); 1272 1273 TraversableAmetysObject attachments = content.getRootAttachments(); 1274 1275 if (attachments != null) 1276 { 1277 result.put("id", attachments.getId()); 1278 if (attachments instanceof ModifiableAmetysObject) 1279 { 1280 result.put("isModifiable", true); 1281 } 1282 if (attachments instanceof ModifiableResourceCollection) 1283 { 1284 result.put("canCreateChild", true); 1285 } 1286 1287 boolean hasChildNodes = false; 1288 boolean hasResources = false; 1289 1290 for (AmetysObject child : attachments.getChildren()) 1291 { 1292 if (child instanceof Resource) 1293 { 1294 hasResources = true; 1295 } 1296 else if (child instanceof ExplorerNode) 1297 { 1298 hasChildNodes = true; 1299 } 1300 } 1301 1302 if (hasChildNodes) 1303 { 1304 result.put("hasChildNodes", true); 1305 } 1306 1307 if (hasResources) 1308 { 1309 result.put("hasResources", true); 1310 } 1311 1312 return result; 1313 } 1314 1315 throw new IllegalArgumentException("Content with id '" + id + "' does not support attachments."); 1316 } 1317 1318 /** 1319 * Determines if the current user has right to delete the content 1320 * @param content The content 1321 * @return true if current user is authorized to delete the content 1322 */ 1323 public boolean canDelete(Content content) 1324 { 1325 return canDelete(content, getRightToDelete()); 1326 } 1327 1328 /** 1329 * Determines if the current user has right to delete the content 1330 * @param content The content 1331 * @param deleteRightId The right's id to check for deletion 1332 * @return true if current user is authorized to delete the content 1333 */ 1334 public boolean canDelete(Content content, String deleteRightId) 1335 { 1336 UserIdentity user = _currentUserProvider.getUser(); 1337 if (_rightManager.hasRight(user, deleteRightId, content) == RightResult.RIGHT_ALLOW) 1338 { 1339 return true; 1340 } 1341 1342 return false; 1343 } 1344 1345 /** 1346 * Add or remove a reaction on a content 1347 * @param contentId The content id 1348 * @param reactionName the reaction name (ex: LIKE) 1349 * @param remove true to remove the reaction, false to add reaction 1350 * @return the result with the current actors of this reaction 1351 */ 1352 @Callable 1353 public Map<String, Object> react(String contentId, String reactionName, boolean remove) 1354 { 1355 Map<String, Object> result = new HashMap<>(); 1356 1357 Content content = _resolver.resolveById(contentId); 1358 1359 if (_rightManager.currentUserHasReadAccess(content)) 1360 { 1361 ReactionType reactionType = ReactionType.valueOf(reactionName); 1362 UserIdentity actor = _currentUserProvider.getUser(); 1363 1364 boolean updated = remove ? unreact(content, actor, reactionType) : react(content, actor, reactionType); 1365 result.put("updated", updated); 1366 result.put("contentId", contentId); 1367 result.put("actors", _userHelper.userIdentities2json(((ReactionableObject) content).getReactionUsers(reactionType))); 1368 } 1369 else 1370 { 1371 result.put("unauthorized", true); 1372 result.put("updated", false); 1373 } 1374 1375 return result; 1376 } 1377 1378 /** 1379 * Add a reaction on a {@link Content}. 1380 * @param content the content 1381 * @param userIdentity the issuer of reaction 1382 * @param reactionType the reaction type 1383 * @return true if a change was made 1384 */ 1385 public boolean react(Content content, UserIdentity userIdentity, ReactionType reactionType) 1386 { 1387 return _addOrRemoveReaction(content, userIdentity, reactionType, false); 1388 } 1389 1390 /** 1391 * Remove reaction if exists on a {@link Content}. 1392 * @param content the content 1393 * @param userIdentity the issuer of reaction 1394 * @param reactionType the reaction type 1395 * @return <code>true</code> if a change was made 1396 */ 1397 public boolean unreact(Content content, UserIdentity userIdentity, ReactionType reactionType) 1398 { 1399 return _addOrRemoveReaction(content, userIdentity, reactionType, true); 1400 } 1401 1402 /** 1403 * Add or remove reaction if exists on the given content. 1404 * @param content the content 1405 * @param userIdentity the issuer of reaction 1406 * @param reactionType the reaction type 1407 * @param remove <code>true</code> if it's to remove the reaction 1408 * @return <code>true</code> if a change was made 1409 */ 1410 protected boolean _addOrRemoveReaction(Content content, UserIdentity userIdentity, ReactionType reactionType, boolean remove) 1411 { 1412 if (content instanceof ReactionableObject) 1413 { 1414 boolean hasChanges = false; 1415 1416 List<UserIdentity> reactionIssuers = ((ReactionableObject) content).getReactionUsers(reactionType); 1417 if (!remove && !reactionIssuers.contains(userIdentity)) 1418 { 1419 ((ReactionableObject) content).addReaction(userIdentity, reactionType); 1420 hasChanges = true; 1421 } 1422 else if (remove && reactionIssuers.contains(userIdentity)) 1423 { 1424 ((ReactionableObject) content).removeReaction(userIdentity, reactionType); 1425 hasChanges = true; 1426 } 1427 1428 if (hasChanges) 1429 { 1430 ((DefaultContent) content).saveChanges(); 1431 1432 Map<String, Object> eventParams = new HashMap<>(); 1433 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 1434 eventParams.put(ObservationConstants.ARGS_REACTION_TYPE, reactionType); 1435 eventParams.put(ObservationConstants.ARGS_REACTION_ISSUER, userIdentity); 1436 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_REACTION_CHANGED, userIdentity, eventParams)); 1437 1438 return true; 1439 } 1440 } 1441 1442 return false; 1443 } 1444 1445 /** 1446 * Add a report on a content 1447 * @param content The content 1448 * @throws IllegalArgumentException if the content is not a {@link ReportableObject} 1449 * @throws AccessDeniedException if the current user has not read access on the given content 1450 */ 1451 public void report(Content content) throws IllegalArgumentException, AccessDeniedException 1452 { 1453 if (_rightManager.currentUserHasReadAccess(content)) 1454 { 1455 if (content instanceof ReportableObject) 1456 { 1457 ((ReportableObject) content).addReport(); 1458 ((DefaultContent) content).saveChanges(); 1459 } 1460 else 1461 { 1462 throw new IllegalArgumentException("Unable to report the content '" + content.getId() + "'. Current user is not authorized to see this content."); 1463 } 1464 } 1465 else 1466 { 1467 throw new AccessDeniedException("Unable to report the content '" + content.getId() + "'. Current user is not authorized to see this content."); 1468 } 1469 } 1470 1471 /** 1472 * Delete contents and force the deletion of invert relations, then log the result. 1473 * @param contents The contents to delete 1474 * @param deleteRightId The deletion right's id to check. Can be null to ignore rights 1475 * @param logger The logger 1476 * @return the number of deleted contents 1477 */ 1478 public int forceDeleteContentsWithLog(List<Content> contents, String deleteRightId, Logger logger) 1479 { 1480 return _logResult(forceDeleteContentsObj(contents, deleteRightId), logger); 1481 } 1482 1483 @SuppressWarnings("unchecked") 1484 private int _logResult(Map<String, Object> result, Logger logger) 1485 { 1486 List<Map<String, Object>> referencedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_REFERENCED + "-contents"); 1487 if (referencedContents.size() > 0) 1488 { 1489 logger.info("The following contents cannot be deleted because they are referenced: {}", referencedContents.stream().map(m -> m.get("id")).collect(Collectors.toList())); 1490 } 1491 1492 List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_LOCKED + "-contents"); 1493 if (lockedContents.size() > 0) 1494 { 1495 logger.info("The following contents cannot be deleted because they are locked: {}", lockedContents.stream().map(m -> m.get("id")).collect(Collectors.toList())); 1496 } 1497 1498 List<Map<String, Object>> unauthorizedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_UNAUTHORIZED + "-contents"); 1499 if (unauthorizedContents.size() > 0) 1500 { 1501 logger.info("The following contents cannot be deleted because they are no authorization: {}", unauthorizedContents.stream().map(m -> m.get("id")).collect(Collectors.toList())); 1502 } 1503 1504 List<Map<String, Object>> undeletedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_UNDELETED + "-contents"); 1505 if (undeletedContents.size() > 0) 1506 { 1507 logger.info("{} contents were not deleted. See previous logs for more information.", undeletedContents.size()); 1508 } 1509 1510 List<Map<String, Object>> deletedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_DELETED + "-contents"); 1511 return deletedContents.size(); 1512 } 1513}