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