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