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