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 (rights = Callable.SKIP_BUILTIN_CHECK) 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 (rights = Callable.SKIP_BUILTIN_CHECK) 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 = Objects.toString(_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 (rights = Callable.SKIP_BUILTIN_CHECK) 558 public Map<String, Object> getContentsProperties (List<String> contentIds, String workspaceName) 559 { 560 // Assume that no read access is checked (required for bus message target) 561 Map<String, Object> result = new HashMap<>(); 562 563 List<Map<String, Object>> contents = new ArrayList<>(); 564 List<String> contentsNotFound = new ArrayList<>(); 565 566 Request request = ContextHelper.getRequest(_context); 567 String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 568 try 569 { 570 if (StringUtils.isNotEmpty(workspaceName)) 571 { 572 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName); 573 } 574 575 for (String contentId : contentIds) 576 { 577 try 578 { 579 Content content = _resolver.resolveById(contentId); 580 contents.add(getContentProperties(content)); 581 } 582 catch (UnknownAmetysObjectException e) 583 { 584 contentsNotFound.add(contentId); 585 } 586 } 587 588 result.put("contents", contents); 589 result.put("contentsNotFound", contentsNotFound); 590 } 591 finally 592 { 593 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace); 594 } 595 596 return result; 597 } 598 599 /** 600 * Get the content properties 601 * @param contentId The id of content 602 * @param workspaceName The workspace name. Can be null to get content in current workspace. 603 * @return The content's properties 604 */ 605 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 606 public Map<String, Object> getContentProperties (String contentId, String workspaceName) 607 { 608 // Assume that no read access is checked (required for bus message target) 609 610 Request request = ContextHelper.getRequest(_context); 611 String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 612 try 613 { 614 if (StringUtils.isNotEmpty(workspaceName)) 615 { 616 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName); 617 } 618 619 Content content = _resolver.resolveById(contentId); 620 return getContentProperties(content); 621 } 622 finally 623 { 624 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace); 625 } 626 } 627 628 /** 629 * Get the content properties 630 * @param content The content 631 * @return The content properties 632 */ 633 public Map<String, Object> getContentProperties (Content content) 634 { 635 Map<String, Object> infos = new HashMap<>(); 636 637 infos.put("id", content.getId()); 638 infos.put("name", content.getName()); 639 infos.put("title", _contentHelper.getTitle(content)); 640 641 if (ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(content.getType(Content.ATTRIBUTE_TITLE).getId())) 642 { 643 infos.put("titleVariants", _contentHelper.getTitleVariants(content)); 644 } 645 infos.put("path", content.getPath()); 646 infos.put("types", content.getTypes()); 647 infos.put("mixins", content.getMixinTypes()); 648 String lang = content.getLanguage(); 649 if (lang != null) 650 { 651 infos.put("lang", lang); 652 } 653 infos.put("creator", _userHelper.user2json(content.getCreator())); 654 infos.put("lastContributor", _userHelper.user2json(content.getLastContributor())); 655 infos.put("creationDate", DateUtils.zonedDateTimeToString(content.getCreationDate())); 656 infos.put("lastModified", DateUtils.zonedDateTimeToString(content.getLastModified())); 657 infos.put("isSimple", _contentHelper.isSimple(content)); 658 infos.put("isReferenceTable", _contentHelper.isReferenceTable(content)); 659 infos.put("parent", _hierarchicalSimpleContentsHelper.getParent(content)); 660 661 if (content instanceof WorkflowAwareContent) 662 { 663 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 664 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 665 666 infos.put("workflowName", workflow.getWorkflowName(waContent.getWorkflowId())); 667 668 List<Integer> workflowSteps = new ArrayList<>(); 669 670 List<Step> currentSteps = workflow.getCurrentSteps(waContent.getWorkflowId()); 671 for (Step step : currentSteps) 672 { 673 workflowSteps.add(step.getStepId()); 674 } 675 infos.put("workflowSteps", workflowSteps); 676 677 int[] availableActions = _contentWorkflowHelper.getAvailableActions(waContent); 678 infos.put("availableActions", availableActions); 679 } 680 681 if (content instanceof ModifiableContent) 682 { 683 infos.put("isModifiable", true); 684 } 685 686 if (content instanceof LockAwareAmetysObject) 687 { 688 LockAwareAmetysObject lockableContent = (LockAwareAmetysObject) content; 689 if (lockableContent.isLocked()) 690 { 691 infos.put("locked", true); 692 infos.put("lockOwner", _userHelper.user2json(lockableContent.getLockOwner())); 693 infos.put("canUnlock", _lockManager.canUnlock(lockableContent)); 694 } 695 } 696 697 infos.put("rights", getUserRights(content)); 698 699 Map<String, Object> additionalData = new HashMap<>(); 700 701 String[] contenttypes = content.getTypes(); 702 for (String cTypeId : contenttypes) 703 { 704 ContentType cType = _contentTypeEP.getExtension(cTypeId); 705 if (cType != null) 706 { 707 additionalData.putAll(cType.getAdditionalData(content)); 708 } 709 } 710 711 if (!additionalData.isEmpty()) 712 { 713 infos.put("additionalData", additionalData); 714 } 715 716 infos.put("isTaggable", content instanceof TaggableAmetysObject); 717 718 if (content instanceof ModifiableDataAwareVersionableAmetysObject) 719 { 720 ModifiableModelLessDataHolder unversionedDataHolder = ((ModifiableDataAwareVersionableAmetysObject) content).getUnversionedDataHolder(); 721 if (unversionedDataHolder.hasValue(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE)) 722 { 723 ZonedDateTime scheduledDate = unversionedDataHolder.getValue(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE); 724 infos.put("scheduledArchivingDate", DateUtils.zonedDateTimeToString(scheduledDate)); 725 } 726 } 727 728 if (content instanceof ReportableObject) 729 { 730 infos.put("reportsCount", ((ReportableObject) content).getReportsCount()); 731 } 732 733 return infos; 734 } 735 736 /** 737 * Get the content's properties for description 738 * @param contentId The id of content 739 * @param workspaceName The workspace name. Can be null to get content in current workspace. 740 * @return The content's properties for description 741 */ 742 @Callable (rights = Callable.READ_ACCESS, rightContext = ContentRightAssignmentContext.ID, paramIndex = 0) 743 public Map<String, Object> getContentDescription (String contentId, String workspaceName) 744 { 745 Request request = ContextHelper.getRequest(_context); 746 String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 747 try 748 { 749 if (StringUtils.isNotEmpty(workspaceName)) 750 { 751 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName); 752 } 753 754 Content content = _resolver.resolveById(contentId); 755 return getContentDescription(content); 756 } 757 finally 758 { 759 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace); 760 } 761 } 762 763 /** 764 *Get the content's properties for description 765 * @param content The content 766 * @return The content's properties for description 767 */ 768 public Map<String, Object> getContentDescription (Content content) 769 { 770 Map<String, Object> infos = new HashMap<>(); 771 772 infos.put("id", content.getId()); 773 infos.put("name", content.getName()); 774 infos.put("title", _contentHelper.getTitle(content)); 775 infos.put("types", content.getTypes()); 776 infos.put("mixins", content.getMixinTypes()); 777 infos.put("lang", content.getLanguage()); 778 infos.put("creator", _userHelper.user2json(content.getCreator())); 779 infos.put("lastContributor", _userHelper.user2json(content.getLastContributor())); 780 infos.put("lastModified", DateUtils.zonedDateTimeToString(content.getLastModified())); 781 infos.put("iconGlyph", _cTypesHelper.getIconGlyph(content)); 782 infos.put("iconDecorator", _cTypesHelper.getIconDecorator(content)); 783 infos.put("smallIcon", _cTypesHelper.getSmallIcon(content)); 784 infos.put("mediumIcon", _cTypesHelper.getMediumIcon(content)); 785 infos.put("largeIcon", _cTypesHelper.getLargeIcon(content)); 786 787 return infos; 788 } 789 790 /** 791 * Get the views of a content plus a view of all the content's data 792 * @param contentId the content's id 793 * @param includeInternal Set to true to include internal views. 794 * @return the views 795 */ 796 @Callable (rights = Callable.READ_ACCESS, rightContext = ContentRightAssignmentContext.ID, paramIndex = 0) 797 public List<Map<String, Object>> getContentViewsAndAllData(String contentId, boolean includeInternal) 798 { 799 List<Map<String, Object>> views = getContentViews(contentId, includeInternal); 800 views.add(_getAllDataView()); 801 return views; 802 } 803 804 private Map<String, Object> _getAllDataView() 805 { 806 Map<String, Object> viewInfos = new HashMap<>(); 807 viewInfos.put("name", ContentTypesHelper.ALL_DATA); 808 viewInfos.put("label", new I18nizableText("plugin.cms", "PLUGINS_CMS_VIEW_ALL_DATA")); 809 viewInfos.put("description", new I18nizableText("plugin.cms", "PLUGINS_CMS_VIEW_ALL_DATA_DESC")); 810 return viewInfos; 811 } 812 813 /** 814 * Get the views of a content 815 * @param contentId the content's id 816 * @param includeInternal Set to true to include internal views. 817 * @return the views 818 */ 819 @Callable (rights = Callable.READ_ACCESS, rightContext = ContentRightAssignmentContext.ID, paramIndex = 0) 820 public List<Map<String, Object>> getContentViews(String contentId, boolean includeInternal) 821 { 822 List<Map<String, Object>> views = new ArrayList<>(); 823 824 Content content = _resolver.resolveById(contentId); 825 String contentTypeId = _cTypesHelper.getContentTypeIdForRendering(content); 826 827 ContentType cType = _contentTypeEP.getExtension(contentTypeId); 828 829 Set<String> viewNames = cType.getViewNames(includeInternal); 830 for (String viewName : viewNames) 831 { 832 View view = cType.getView(viewName); 833 834 Map<String, Object> viewInfos = new HashMap<>(); 835 viewInfos.put("name", viewName); 836 viewInfos.put("label", view.getLabel()); 837 viewInfos.put("description", view.getDescription()); 838 views.add(viewInfos); 839 } 840 841 return views; 842 } 843 844 /** 845 * Get the user rights on content 846 * @param content The content 847 * @return The user's rights 848 */ 849 protected Set<String> getUserRights (Content content) 850 { 851 UserIdentity user = _currentUserProvider.getUser(); 852 return _rightManager.getUserRights(user, content); 853 } 854 855 /** 856 * Get the tags of contents 857 * @param contentIds The content's ids 858 * @return the tags 859 */ 860 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 861 public Set<String> getTags (List<String> contentIds) 862 { 863 Set<String> tags = new HashSet<>(); 864 865 for (String contentId : contentIds) 866 { 867 Content content = _resolver.resolveById(contentId); 868 if (_rightManager.currentUserHasReadAccess(content)) 869 { 870 tags.addAll(content.getTags()); 871 } 872 } 873 874 return tags; 875 } 876 877 /** 878 * Tag a list of contents with the given tags 879 * @param contentIds The ids of contents to tag 880 * @param tagNames The tags 881 * @param contextualParameters The contextual parameters 882 * @return the result 883 */ 884 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 885 public Map<String, Object> tag (List<String> contentIds, List<String> tagNames, Map<String, Object> contextualParameters) 886 { 887 return tag(contentIds, tagNames, TagMode.REPLACE, contextualParameters, false); 888 } 889 890 /** 891 * Tag a list of contents 892 * @param contentIds The ids of contents to tag 893 * @param tagNames The tags 894 * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags. 895 * @param contextualParameters The contextual parameters 896 * @return the result 897 */ 898 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 899 public Map<String, Object> tag (List<String> contentIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters) 900 { 901 return tag(contentIds, tagNames, TagMode.valueOf(mode), contextualParameters, false); 902 } 903 904 /** 905 * Tag a list of contents 906 * @param contentIds The ids of contents to tag 907 * @param tagNames The tags 908 * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags. 909 * @param contextualParameters The contextual parameters 910 * @param ignoreRights <code>true</code> to ignore the rights on tag 911 * @return the result 912 */ 913 public Map<String, Object> tag (List<String> contentIds, List<String> tagNames, TagMode mode, Map<String, Object> contextualParameters, boolean ignoreRights) 914 { 915 Map<String, Object> result = new HashMap<>(); 916 917 result.put("notaggable-contents", new ArrayList<>()); 918 result.put("invalid-tags", new ArrayList<>()); 919 result.put("allright-contents", new ArrayList<>()); 920 result.put("locked-contents", new ArrayList<>()); 921 result.put("noright-contents", new ArrayList<>()); 922 923 for (String contentId : contentIds) 924 { 925 Content content = _resolver.resolveById(contentId); 926 927 Map<String, Object> content2json = new HashMap<>(); 928 content2json.put("id", content.getId()); 929 content2json.put("title", _contentHelper.getTitle(content)); 930 931 if (!ignoreRights && !_hasTagRights(content, tagNames, mode, contextualParameters)) 932 { 933 @SuppressWarnings("unchecked") 934 List<Map<String, Object>> noRightContents = (List<Map<String, Object>>) result.get("noright-contents"); 935 noRightContents.add(content2json); 936 } 937 else if (content instanceof TaggableAmetysObject) 938 { 939 TaggableAmetysObject mContent = (TaggableAmetysObject) content; 940 941 boolean wasLocked = false; 942 943 if (content instanceof LockableAmetysObject) 944 { 945 LockableAmetysObject lockableContent = (LockableAmetysObject) content; 946 UserIdentity user = _currentUserProvider.getUser(); 947 if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, user)) 948 { 949 @SuppressWarnings("unchecked") 950 List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) result.get("locked-contents"); 951 content2json.put("lockOwner", lockableContent.getLockOwner()); 952 lockedContents.add(content2json); 953 954 // Stop process 955 continue; 956 } 957 958 if (lockableContent.isLocked()) 959 { 960 wasLocked = true; 961 lockableContent.unlock(); 962 } 963 } 964 965 Set<String> oldTags = mContent.getTags(); 966 _removeAllTagsInReplaceMode(mContent, mode, oldTags); 967 968 // Then set new tags 969 for (String tagName : tagNames) 970 { 971 if (_isTagValid(tagName, contextualParameters)) 972 { 973 if (TagMode.REMOVE.equals(mode)) 974 { 975 mContent.untag(tagName); 976 } 977 else if (TagMode.REPLACE.equals(mode) || !oldTags.contains(tagName)) 978 { 979 mContent.tag(tagName); 980 } 981 982 } 983 else 984 { 985 @SuppressWarnings("unchecked") 986 List<String> invalidTags = (List<String>) result.get("invalid-tags"); 987 invalidTags.add(tagName); 988 } 989 } 990 991 ((ModifiableAmetysObject) content).saveChanges(); 992 993 if (wasLocked) 994 { 995 // Relock content if it was locked before tagging 996 ((LockableAmetysObject) content).lock(); 997 } 998 999 content2json.put("tags", content.getTags()); 1000 @SuppressWarnings("unchecked") 1001 List<Map<String, Object>> allRightPages = (List<Map<String, Object>>) result.get("allright-contents"); 1002 allRightPages.add(content2json); 1003 1004 if (!oldTags.equals(content.getTags())) 1005 { 1006 // Notify observers that the content has been tagged 1007 Map<String, Object> eventParams = new HashMap<>(); 1008 eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT, content); 1009 eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT_ID, content.getId()); 1010 eventParams.put("content.tags", content.getTags()); 1011 eventParams.put("content.old.tags", oldTags); 1012 _observationManager.notify(new Event(org.ametys.cms.ObservationConstants.EVENT_CONTENT_TAGGED, _currentUserProvider.getUser(), eventParams)); 1013 } 1014 } 1015 else 1016 { 1017 @SuppressWarnings("unchecked") 1018 List<Map<String, Object>> notaggableContents = (List<Map<String, Object>>) result.get("notaggable-contents"); 1019 notaggableContents.add(content2json); 1020 } 1021 } 1022 1023 return result; 1024 } 1025 1026 private boolean _hasTagRights(Content content, List<String> tagNames, TagMode mode, Map<String, Object> contextualParameters) 1027 { 1028 List<CMSTag> tags = tagNames.stream() 1029 .map(t -> _tagProvider.getTag(t, contextualParameters)) 1030 .toList(); 1031 1032 // In case of replace, only check the right on tag that are modified, ie added or removed tags 1033 if (TagMode.REPLACE.equals(mode)) 1034 { 1035 List<CMSTag> existingTags = content.getTags().stream() 1036 .map(t -> _tagProvider.getTag(t, contextualParameters)) 1037 .toList(); 1038 1039 tags = new ArrayList<>(CollectionUtils.disjunction(tags, existingTags)); 1040 } 1041 1042 boolean hasPublicTagRight = _rightManager.currentUserHasRight("CMS_Rights_Content_Tag", content) == RightResult.RIGHT_ALLOW; 1043 boolean hasPrivateTagRight = _rightManager.currentUserHasRight("CMS_Rights_Content_Private_Tag", content) == RightResult.RIGHT_ALLOW; 1044 1045 // Test if the current user has the right to tag public tag on content only if there are at least one public tag 1046 boolean hasRight = TagHelper.filterTags(tags, TagVisibility.PUBLIC, "CONTENT").isEmpty() || hasPublicTagRight || hasPrivateTagRight; 1047 1048 // Test if the current user has the right to tag private tag on content only if there are at least one private tag 1049 return hasRight && (TagHelper.filterTags(tags, TagVisibility.PRIVATE, "CONTENT").isEmpty() || hasPrivateTagRight); 1050 } 1051 1052 /** 1053 * Remove all tags from the given content if tagMode is equals to REPLACE. 1054 * @param mContent The content 1055 * @param tagMode The tag 1056 * @param oldTags Tags to remove 1057 */ 1058 protected void _removeAllTagsInReplaceMode(TaggableAmetysObject mContent, TagMode tagMode, Set<String> oldTags) 1059 { 1060 if (TagMode.REPLACE.equals(tagMode)) 1061 { 1062 // First delete old tags 1063 for (String tagName : oldTags) 1064 { 1065 mContent.untag(tagName); 1066 } 1067 } 1068 } 1069 1070 /** 1071 * Is the tag a content tag 1072 * @param tagName The tag name 1073 * @param contextualParameters The contextual parameters 1074 * @return true if the tag is a valid content tag 1075 */ 1076 protected boolean _isTagValid (String tagName, Map<String, Object> contextualParameters) 1077 { 1078 CMSTag tag = _tagProvider.getTag(tagName, contextualParameters); 1079 return tag.getTarget().getName().equals("CONTENT"); 1080 } 1081 1082 /** 1083 * Copy a content. 1084 * @param originalContent the original content. 1085 * @param parent the object in which to create a content. 1086 * @param name the content name. 1087 * @param initWorkflowActionId The initial workflow action id 1088 * @param context The context of the data to copy 1089 * @return the copied content. 1090 * @throws AmetysRepositoryException If an error occured 1091 */ 1092 public ModifiableContent copy(DefaultContent originalContent, ModifiableTraversableAmetysObject parent, String name, int initWorkflowActionId, DataContext context) throws AmetysRepositoryException 1093 { 1094 return copy(originalContent, parent, name, null, initWorkflowActionId, context); 1095 } 1096 1097 /** 1098 * Copy a content. 1099 * @param originalContent the original content. 1100 * @param parent the object in which to create a content. 1101 * @param name the content name. 1102 * @param lang the content language. If null, the content language will be the same of the original content 1103 * @param initWorkflowActionId The initial workflow action id 1104 * @param context The context of the data to copy 1105 * @return the copied content. 1106 * @throws AmetysRepositoryException If an error occured 1107 */ 1108 public ModifiableContent copy(DefaultContent originalContent, ModifiableTraversableAmetysObject parent, String name, String lang, int initWorkflowActionId, DataContext context) throws AmetysRepositoryException 1109 { 1110 return copy(originalContent, parent, name, lang, initWorkflowActionId, true, true, false, false, context); 1111 } 1112 1113 /** 1114 * Copy a content. 1115 * @param originalContent the original content. 1116 * @param parent the object in which to create a content. 1117 * @param name the content name. 1118 * @param lang the content language. If null, the content language will be the same of the original content 1119 * @param initWorkflowActionId The initial workflow action id 1120 * @param notifyObservers Set to false to do not fire observer events 1121 * @param checkpoint true to check the content in if it is versionable 1122 * @param waitAsyncObservers true to wait for asynchronous observers to complete 1123 * @param copyACL true to copy ACL of source content 1124 * @param context The context of the data to copy 1125 * @return the copied content. 1126 * @throws AmetysRepositoryException If an error occured 1127 */ 1128 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 1129 { 1130 try 1131 { 1132 String originalName = name == null ? originalContent.getName() : name; 1133 String contentName = originalName; 1134 int index = 2; 1135 while (parent.hasChild(contentName)) 1136 { 1137 contentName = originalName + "-" + (index++); 1138 } 1139 1140 String originalContentType = originalContent.getNode().getPrimaryNodeType().getName(); 1141 1142 ModifiableContent content = parent.createChild(contentName, originalContentType); 1143 1144 String targetLanguage = lang == null ? originalContent.getLanguage() : lang; 1145 if (targetLanguage != null) 1146 { 1147 content.setLanguage(targetLanguage); 1148 } 1149 1150 content.setTypes(originalContent.getTypes()); 1151 1152 _modifiableContentHelper.copyTitle(originalContent, content); 1153 1154 if (originalContent instanceof WorkflowAwareContent) 1155 { 1156 WorkflowAwareContent waOriginalContent = (WorkflowAwareContent) originalContent; 1157 AmetysObjectWorkflow originalContentWorkflow = _workflowProvider.getAmetysObjectWorkflow(waOriginalContent); 1158 String workflowName = originalContentWorkflow.getWorkflowName(waOriginalContent.getWorkflowId()); 1159 1160 // Initialize new content workflow 1161 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 1162 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 1163 1164 HashMap<String, Object> inputs = new HashMap<>(); 1165 // Provide the content key 1166 inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<>()); 1167 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, waContent); 1168 1169 long workflowId = workflow.initialize(workflowName, initWorkflowActionId, inputs); 1170 1171 // Remove workflow id if exists before updating it 1172 WorkflowAwareContentHelper.removeWorkflowId(waContent); 1173 waContent.setWorkflowId(workflowId); 1174 1175 // Set the current step ID property 1176 Step currentStep = (Step) workflow.getCurrentSteps(workflowId).iterator().next(); 1177 waContent.setCurrentStepId(currentStep.getStepId()); 1178 1179 Node workflowEntryNode = null; 1180 Node node = waContent.getNode(); 1181 Session session = node.getSession(); 1182 1183 1184 try 1185 { 1186 AbstractJackrabbitWorkflowStore workflowStore = (AbstractJackrabbitWorkflowStore) workflow.getConfiguration().getWorkflowStore(); 1187 1188 if (workflowStore instanceof AmetysObjectWorkflowStore) 1189 { 1190 AmetysObjectWorkflowStore ametysObjectWorkflowStore = (AmetysObjectWorkflowStore) workflowStore; 1191 ametysObjectWorkflowStore.bindAmetysObject(waContent); 1192 } 1193 1194 workflowEntryNode = workflowStore.getEntryNode(session, workflowId); 1195 workflowEntryNode.setProperty("ametys-internal:initialActionId", initWorkflowActionId); 1196 } 1197 catch (RepositoryException e) 1198 { 1199 throw new AmetysRepositoryException("Unable to link the workflow to the content", e); 1200 } 1201 } 1202 1203 // Copy attributes 1204 originalContent.copyTo(content, context); 1205 1206 // Copy attachments 1207 _copyAttachments(originalContent, content); 1208 1209 if (copyACL) 1210 { 1211 _copyACL(originalContent, content); 1212 } 1213 1214 UserIdentity currentUser = _currentUserProvider.getUser(); 1215 if (currentUser != null) 1216 { 1217 content.setCreator(currentUser); 1218 content.setLastContributor(currentUser); 1219 content.setLastModified(ZonedDateTime.now()); 1220 content.setCreationDate(ZonedDateTime.now()); 1221 } 1222 1223 parent.saveChanges(); 1224 1225 // Create a new version 1226 if (checkpoint && content instanceof VersionableAmetysObject versionableContent) 1227 { 1228 versionableContent.checkpoint(); 1229 } 1230 1231 if (notifyObservers) 1232 { 1233 notifyContentCopied(content, waitAsyncObservers); 1234 } 1235 1236 return content; 1237 } 1238 catch (WorkflowException e) 1239 { 1240 throw new AmetysRepositoryException(e); 1241 } 1242 catch (RepositoryException e) 1243 { 1244 throw new AmetysRepositoryException(e); 1245 } 1246 } 1247 1248 /** 1249 * Copy the attachments of a content 1250 * @param srcContent The source content 1251 * @param targetContent The target content 1252 */ 1253 protected void _copyAttachments(Content srcContent, Content targetContent) 1254 { 1255 ResourceCollection srcRootAttachments = srcContent.getRootAttachments(); 1256 if (srcRootAttachments == null) 1257 { 1258 // There are no attachments to copy 1259 return; 1260 } 1261 1262 ResourceCollection targetRootAttachments = targetContent.getRootAttachments(); 1263 if (targetRootAttachments == null) 1264 { 1265 // The target is an (unmodifiable) old version and the attachments root is missing 1266 return; 1267 } 1268 1269 AmetysObjectIterable<AmetysObject> children = srcRootAttachments.getChildren(); 1270 for (AmetysObject child : children) 1271 { 1272 if (child instanceof CopiableAmetysObject) 1273 { 1274 try 1275 { 1276 ((CopiableAmetysObject) child).copyTo((ModifiableTraversableAmetysObject) targetRootAttachments, child.getName()); 1277 } 1278 catch (AmetysRepositoryException e) 1279 { 1280 getLogger().error("Failed to copy attachments at path " + child.getPath() + " from content " + srcContent + " to content " + targetContent, e); 1281 } 1282 } 1283 } 1284 } 1285 1286 /** 1287 * Copy the ACL of a content 1288 * @param srcContent The source content 1289 * @param targetContent The target content 1290 */ 1291 protected void _copyACL(Content srcContent, Content targetContent) 1292 { 1293 if (srcContent instanceof DefaultContent && targetContent instanceof DefaultContent) 1294 { 1295 Node srcNode = ((DefaultContent) srcContent).getNode(); 1296 Node targetNode = ((DefaultContent) targetContent).getNode(); 1297 1298 try 1299 { 1300 String aclNodeName = "ametys-internal:acl"; 1301 if (srcNode.hasNode(aclNodeName)) 1302 { 1303 Node aclNode = srcNode.getNode(aclNodeName); 1304 aclNode.getSession().getWorkspace().copy(aclNode.getPath(), targetNode.getPath() + "/" + aclNodeName); 1305 } 1306 } 1307 catch (RepositoryException e) 1308 { 1309 getLogger().error("Failed to copy ACL from content " + srcContent + " to content " + targetContent, e); 1310 } 1311 } 1312 } 1313 1314 /** 1315 * Notify observers that the content has been created 1316 * @param content The content added 1317 * @param waitAsyncObservers true to wait for asynchonous observers to finish 1318 * @throws WorkflowException If an error occurred 1319 */ 1320 public void notifyContentCopied(Content content, boolean waitAsyncObservers) throws WorkflowException 1321 { 1322 Map<String, Object> eventParams = new HashMap<>(); 1323 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 1324 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 1325 1326 List<Future> futures = _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_ADDED, _currentUserProvider.getUser(), eventParams)); 1327 futures.addAll(_observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_WORKFLOW_CHANGED, _currentUserProvider.getUser(), eventParams))); 1328 1329 if (waitAsyncObservers) 1330 { 1331 // Wait for asynchonous observers to finish 1332 for (Future future : futures) 1333 { 1334 try 1335 { 1336 future.get(); 1337 } 1338 catch (ExecutionException | InterruptedException e) 1339 { 1340 getLogger().error(String.format("Error while waiting for async observer to complete")); 1341 } 1342 } 1343 } 1344 } 1345 1346 /** 1347 * Returns the content's attachments root node 1348 * @param id the content's id 1349 * @return The attachments' root node informations 1350 */ 1351 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 1352 public Map<String, Object> getAttachmentsRootNode (String id) 1353 { 1354 Map<String, Object> result = new HashMap<>(); 1355 1356 Content content = _resolver.resolveById(id); 1357 1358 result.put("title", _contentHelper.getTitle(content)); 1359 result.put("contentId", content.getId()); 1360 1361 if (_rightManager.currentUserHasRight("CMS_Rights_Content_Attachments", content) != RightResult.RIGHT_ALLOW 1362 && _rightManager.currentUserHasRight("CMS_Rights_Content_AttachmentHandle", content) != RightResult.RIGHT_ALLOW) 1363 { 1364 return result; 1365 } 1366 1367 TraversableAmetysObject attachments = content.getRootAttachments(); 1368 1369 if (attachments != null) 1370 { 1371 result.put("id", attachments.getId()); 1372 if (attachments instanceof ModifiableAmetysObject) 1373 { 1374 result.put("isModifiable", true); 1375 } 1376 if (attachments instanceof ModifiableResourceCollection) 1377 { 1378 result.put("canCreateChild", true); 1379 } 1380 1381 boolean hasChildNodes = false; 1382 boolean hasResources = false; 1383 1384 for (AmetysObject child : attachments.getChildren()) 1385 { 1386 if (child instanceof Resource) 1387 { 1388 hasResources = true; 1389 } 1390 else if (child instanceof ExplorerNode) 1391 { 1392 hasChildNodes = true; 1393 } 1394 } 1395 1396 if (hasChildNodes) 1397 { 1398 result.put("hasChildNodes", true); 1399 } 1400 1401 if (hasResources) 1402 { 1403 result.put("hasResources", true); 1404 } 1405 1406 return result; 1407 } 1408 1409 throw new IllegalArgumentException("Content with id '" + id + "' does not support attachments."); 1410 } 1411 1412 /** 1413 * Determines if the current user has right to delete the content 1414 * @param content The content 1415 * @return true if current user is authorized to delete the content 1416 */ 1417 public boolean canDelete(Content content) 1418 { 1419 return canDelete(content, getRightToDelete()); 1420 } 1421 1422 /** 1423 * Determines if the current user has right to delete the content 1424 * @param content The content 1425 * @param deleteRightId The right's id to check for deletion 1426 * @return true if current user is authorized to delete the content 1427 */ 1428 public boolean canDelete(Content content, String deleteRightId) 1429 { 1430 UserIdentity user = _currentUserProvider.getUser(); 1431 if (_rightManager.hasRight(user, deleteRightId, content) == RightResult.RIGHT_ALLOW) 1432 { 1433 return true; 1434 } 1435 1436 return false; 1437 } 1438 1439 /** 1440 * Add or remove a reaction on a content 1441 * @param contentId The content id 1442 * @param reactionName the reaction name (ex: LIKE) 1443 * @param remove true to remove the reaction, false to add reaction 1444 * @return the result with the current actors of this reaction 1445 */ 1446 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 1447 public Map<String, Object> react(String contentId, String reactionName, boolean remove) 1448 { 1449 Map<String, Object> result = new HashMap<>(); 1450 1451 Content content = _resolver.resolveById(contentId); 1452 1453 if (_rightManager.currentUserHasReadAccess(content)) 1454 { 1455 ReactionType reactionType = ReactionType.valueOf(reactionName); 1456 UserIdentity actor = _currentUserProvider.getUser(); 1457 1458 boolean updated = remove ? unreact(content, actor, reactionType) : react(content, actor, reactionType); 1459 result.put("updated", updated); 1460 result.put("contentId", contentId); 1461 result.put("actors", _userHelper.userIdentities2json(((ReactionableObject) content).getReactionUsers(reactionType))); 1462 } 1463 else 1464 { 1465 result.put("unauthorized", true); 1466 result.put("updated", false); 1467 } 1468 1469 return result; 1470 } 1471 1472 /** 1473 * Get the list of users who react to content 1474 * @param contentId The content id 1475 * @param reactionName the reaction name (ex: LIKE) 1476 * @return the list of users 1477 */ 1478 @Callable (rights = Callable.READ_ACCESS, rightContext = ContentRightAssignmentContext.ID, paramIndex = 0) 1479 public List<Map<String, Object>> getReactionUsers(String contentId, String reactionName) 1480 { 1481 Content content = _resolver.resolveById(contentId); 1482 if (content instanceof ReactionableObject reactionableContent) 1483 { 1484 ReactionType reactionType = ReactionType.valueOf(reactionName); 1485 List<UserIdentity> reactionUsers = reactionableContent.getReactionUsers(reactionType); 1486 1487 return reactionUsers.stream().map(_userHelper::user2json).toList(); 1488 } 1489 1490 return List.of(); 1491 } 1492 1493 /** 1494 * Add a reaction on a {@link Content}. 1495 * @param content the content 1496 * @param userIdentity the issuer of reaction 1497 * @param reactionType the reaction type 1498 * @return true if a change was made 1499 */ 1500 public boolean react(Content content, UserIdentity userIdentity, ReactionType reactionType) 1501 { 1502 return _addOrRemoveReaction(content, userIdentity, reactionType, false); 1503 } 1504 1505 /** 1506 * Remove reaction if exists on a {@link Content}. 1507 * @param content the content 1508 * @param userIdentity the issuer of reaction 1509 * @param reactionType the reaction type 1510 * @return <code>true</code> if a change was made 1511 */ 1512 public boolean unreact(Content content, UserIdentity userIdentity, ReactionType reactionType) 1513 { 1514 return _addOrRemoveReaction(content, userIdentity, reactionType, true); 1515 } 1516 1517 /** 1518 * Add or remove reaction if exists on the given content. 1519 * @param content the content 1520 * @param userIdentity the issuer of reaction 1521 * @param reactionType the reaction type 1522 * @param remove <code>true</code> if it's to remove the reaction 1523 * @return <code>true</code> if a change was made 1524 */ 1525 protected boolean _addOrRemoveReaction(Content content, UserIdentity userIdentity, ReactionType reactionType, boolean remove) 1526 { 1527 if (content instanceof ReactionableObject) 1528 { 1529 boolean hasChanges = false; 1530 1531 List<UserIdentity> reactionIssuers = ((ReactionableObject) content).getReactionUsers(reactionType); 1532 if (!remove && !reactionIssuers.contains(userIdentity)) 1533 { 1534 ((ReactionableObject) content).addReaction(userIdentity, reactionType); 1535 hasChanges = true; 1536 } 1537 else if (remove && reactionIssuers.contains(userIdentity)) 1538 { 1539 ((ReactionableObject) content).removeReaction(userIdentity, reactionType); 1540 hasChanges = true; 1541 } 1542 1543 if (hasChanges) 1544 { 1545 ((DefaultContent) content).saveChanges(); 1546 1547 Map<String, Object> eventParams = new HashMap<>(); 1548 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 1549 eventParams.put(ObservationConstants.ARGS_REACTION_TYPE, reactionType); 1550 eventParams.put(ObservationConstants.ARGS_REACTION_ISSUER, userIdentity); 1551 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_REACTION_CHANGED, userIdentity, eventParams)); 1552 1553 return true; 1554 } 1555 } 1556 1557 return false; 1558 } 1559 1560 /** 1561 * Add a report on a content 1562 * @param content The content 1563 * @throws IllegalArgumentException if the content is not a {@link ReportableObject} 1564 * @throws AccessDeniedException if the current user has not read access on the given content 1565 */ 1566 public void report(Content content) throws IllegalArgumentException, AccessDeniedException 1567 { 1568 if (_rightManager.currentUserHasReadAccess(content)) 1569 { 1570 if (content instanceof ReportableObject) 1571 { 1572 ((ReportableObject) content).addReport(); 1573 ((DefaultContent) content).saveChanges(); 1574 } 1575 else 1576 { 1577 throw new IllegalArgumentException("Unable to report the content '" + content.getId() + "'. Current user is not authorized to see this content."); 1578 } 1579 } 1580 else 1581 { 1582 throw new AccessDeniedException("Unable to report the content '" + content.getId() + "'. Current user is not authorized to see this content."); 1583 } 1584 } 1585 1586 /** 1587 * Delete contents and force the deletion of invert relations, then log the result. 1588 * @param contents The contents to delete 1589 * @param deleteRightId The deletion right's id to check. Can be null to ignore rights 1590 * @param logger The logger 1591 * @return the number of deleted contents 1592 */ 1593 public int forceDeleteContentsWithLog(List<Content> contents, String deleteRightId, Logger logger) 1594 { 1595 return _logResult(forceDeleteContentsObj(contents, deleteRightId), logger); 1596 } 1597 1598 @SuppressWarnings("unchecked") 1599 private int _logResult(Map<String, Object> result, Logger logger) 1600 { 1601 List<Map<String, Object>> referencedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_REFERENCED + "-contents"); 1602 if (referencedContents.size() > 0) 1603 { 1604 logger.info("The following contents cannot be deleted because they are referenced: {}", referencedContents.stream().map(m -> m.get("id")).collect(Collectors.toList())); 1605 } 1606 1607 List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_LOCKED + "-contents"); 1608 if (lockedContents.size() > 0) 1609 { 1610 logger.info("The following contents cannot be deleted because they are locked: {}", lockedContents.stream().map(m -> m.get("id")).collect(Collectors.toList())); 1611 } 1612 1613 List<Map<String, Object>> unauthorizedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_UNAUTHORIZED + "-contents"); 1614 if (unauthorizedContents.size() > 0) 1615 { 1616 logger.info("The following contents cannot be deleted because they are no authorization: {}", unauthorizedContents.stream().map(m -> m.get("id")).collect(Collectors.toList())); 1617 } 1618 1619 List<Map<String, Object>> undeletedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_UNDELETED + "-contents"); 1620 if (undeletedContents.size() > 0) 1621 { 1622 logger.info("{} contents were not deleted. See previous logs for more information.", undeletedContents.size()); 1623 } 1624 1625 List<Map<String, Object>> deletedContents = (List<Map<String, Object>>) result.get(_CONTENT_DELETION_STATUS_DELETED + "-contents"); 1626 return deletedContents.size(); 1627 } 1628}