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