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