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.web.repository.page; 017 018import java.util.HashMap; 019import java.util.Map; 020import java.util.Set; 021 022import javax.jcr.RepositoryException; 023 024import org.apache.avalon.framework.component.Component; 025import org.apache.avalon.framework.logger.AbstractLogEnabled; 026import org.apache.avalon.framework.service.ServiceException; 027import org.apache.avalon.framework.service.ServiceManager; 028import org.apache.avalon.framework.service.Serviceable; 029import org.apache.commons.lang.ArrayUtils; 030import org.apache.commons.lang.StringUtils; 031import org.apache.commons.lang3.LocaleUtils; 032 033import org.ametys.cms.CmsConstants; 034import org.ametys.cms.contenttype.ContentType; 035import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 036import org.ametys.cms.repository.Content; 037import org.ametys.cms.repository.DefaultContent; 038import org.ametys.cms.repository.WorkflowAwareContent; 039import org.ametys.core.observation.Event; 040import org.ametys.core.observation.ObservationManager; 041import org.ametys.core.right.RightManager; 042import org.ametys.core.right.RightManager.RightResult; 043import org.ametys.core.ui.Callable; 044import org.ametys.core.user.CurrentUserProvider; 045import org.ametys.core.user.UserIdentity; 046import org.ametys.plugins.repository.AmetysObjectResolver; 047import org.ametys.plugins.repository.AmetysRepositoryException; 048import org.ametys.plugins.repository.UnknownAmetysObjectException; 049import org.ametys.plugins.repository.jcr.JCRAmetysObject; 050import org.ametys.plugins.workflow.support.WorkflowProvider; 051import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 052import org.ametys.runtime.authentication.AccessDeniedException; 053import org.ametys.web.ObservationConstants; 054import org.ametys.web.repository.content.SharedContent; 055import org.ametys.web.repository.content.WebContent; 056import org.ametys.web.repository.content.jcr.DefaultSharedContent; 057import org.ametys.web.repository.content.shared.SharedContentManager; 058import org.ametys.web.repository.page.ZoneItem.ZoneType; 059import org.ametys.web.rights.PageRightAssignmentContext; 060import org.ametys.web.service.Service; 061import org.ametys.web.service.ServiceExtensionPoint; 062 063/** 064 * DAO for manipulating {@link Zone} and {@link ZoneItem} 065 * 066 */ 067public class ZoneDAO extends AbstractLogEnabled implements Serviceable, Component 068{ 069 /** Avalon Role */ 070 public static final String ROLE = ZoneDAO.class.getName(); 071 072 private AmetysObjectResolver _resolver; 073 private ObservationManager _observationManager; 074 private CurrentUserProvider _currentUserProvider; 075 private WorkflowProvider _workflowProvider; 076 private SharedContentManager _sharedContentManager; 077 private ContentTypesAssignmentHandler _cTypesAssignmentHandler; 078 private ServicesAssignmentHandler _serviceAssignmentHandler; 079 private ContentTypeExtensionPoint _cTypeEP; 080 private ServiceExtensionPoint _serviceEP; 081 private RightManager _rightManager; 082 083 @Override 084 public void service(ServiceManager smanager) throws ServiceException 085 { 086 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 087 _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE); 088 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 089 _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE); 090 _sharedContentManager = (SharedContentManager) smanager.lookup(SharedContentManager.ROLE); 091 _cTypesAssignmentHandler = (ContentTypesAssignmentHandler) smanager.lookup(ContentTypesAssignmentHandler.ROLE); 092 _serviceAssignmentHandler = (ServicesAssignmentHandler) smanager.lookup(ServicesAssignmentHandler.ROLE); 093 _cTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 094 _serviceEP = (ServiceExtensionPoint) smanager.lookup(ServiceExtensionPoint.ROLE); 095 _rightManager = (RightManager) smanager.lookup(RightManager.ROLE); 096 } 097 098 /** 099 * Insert a shared content into a zone 100 * @param pageId The page id 101 * @param zoneName The zone name 102 * @param contentId The content id to insert 103 * @param viewName The view name 104 * @return The result map 105 */ 106 @Callable (rights = "Web_Rights_Page_AddSharedContent", paramIndex = 0, rightContext = PageRightAssignmentContext.ID) 107 public Map<String, Object> addSharedContent (String pageId, String zoneName, String contentId, String viewName) 108 { 109 Map<String, Object> result = new HashMap<>(); 110 111 SitemapElement sitemapElement = _resolver.resolveById(pageId); 112 113 if (!(sitemapElement instanceof ModifiableSitemapElement mSitemapElement)) 114 { 115 throw new IllegalArgumentException("Can not add a shared content on a non-modifiable page " + pageId); 116 } 117 118 if (sitemapElement.getTemplate() == null) 119 { 120 throw new IllegalArgumentException("Page '" + pageId + "' is not a container page: unable to add a shared content"); 121 } 122 123 ModifiableZone zone; 124 if (mSitemapElement.hasZone(zoneName)) 125 { 126 zone = mSitemapElement.getZone(zoneName); 127 } 128 else 129 { 130 zone = mSitemapElement.createZone(zoneName); 131 } 132 133 Content content = _resolver.resolveById(contentId); 134 String contentSiteName = null; 135 if (content instanceof WebContent) 136 { 137 contentSiteName = ((WebContent) content).getSiteName(); 138 } 139 140 // Check that the content type is an authorized content type. 141 Set<String> validCTypes = _cTypesAssignmentHandler.getAvailableContentTypes(mSitemapElement, zoneName, true); 142 for (String contentTypeId : content.getTypes()) 143 { 144 if (!validCTypes.contains(contentTypeId)) 145 { 146 ContentType cType = _cTypeEP.getExtension(contentTypeId); 147 result.put("error", true); 148 result.put("invalid-contenttype", cType.getLabel()); 149 return result; 150 } 151 } 152 153 ZoneItem zoneItem = null; 154 if (contentSiteName == null || mSitemapElement.getSiteName().equals(contentSiteName)) 155 { 156 // The content comes from the same site as the page: insert the content as a new zone item. 157 zoneItem = addContentReference(zone, content, viewName); 158 } 159 else 160 { 161 // The content is from a different site: create a shared content in the zone. 162 if (!(content instanceof DefaultContent) || content instanceof SharedContent) 163 { 164 throw new IllegalArgumentException("The source content must be a DefaultContent but not a SharedContent."); 165 } 166 167 DefaultContent defaultContent = (DefaultContent) content; 168 169 if (!ArrayUtils.contains(defaultContent.getAllLabels(), CmsConstants.LIVE_LABEL)) 170 { 171 result.put("error", true); 172 result.put("unpublished-content", defaultContent.getTitle(LocaleUtils.toLocale(mSitemapElement.getSitemapName()))); 173 return result; 174 } 175 176 zoneItem = createSharedContent(zone, defaultContent, viewName); 177 } 178 179 mSitemapElement.saveChanges(); 180 181 String zoneItemId = zoneItem.getId(); 182 result.put("zoneItemId", zoneItemId); 183 184 Map<String, Object> eventParams = new HashMap<>(); 185 eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, mSitemapElement); 186 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItemId); 187 eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, ZoneType.CONTENT); 188 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_CONTENT, zoneItem.getContent()); 189 _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_ADDED, _currentUserProvider.getUser(), eventParams)); 190 191 return result; 192 } 193 194 /** 195 * Add the given content as a zone item in the given zone. 196 * @param zone the zone to add the content in. 197 * @param content the content to add. 198 * @param viewName the view name. 199 * @return the added zone item 200 */ 201 protected ZoneItem addContentReference(ModifiableZone zone, Content content, String viewName) 202 { 203 ModifiableZoneItem zoneItem = zone.addZoneItem(); 204 zoneItem.setType(ZoneType.CONTENT); 205 206 zoneItem.setContent(content); 207 zoneItem.setViewName(viewName); 208 209 return zoneItem; 210 } 211 212 /** 213 * Create a shared content referencing the given content and add the shared one to the zone. 214 * @param zone the zone to create the shared content in. 215 * @param originalContent the original content. 216 * @param viewName the view name. 217 * @return the added zone item 218 */ 219 protected ZoneItem createSharedContent(ModifiableZone zone, DefaultContent originalContent, String viewName) 220 { 221 ModifiableZoneItem zoneItem = zone.addZoneItem(); 222 zoneItem.setType(ZoneType.CONTENT); 223 224 DefaultSharedContent content = _sharedContentManager.createSharedContent(zone.getSitemapElement().getSite(), originalContent); 225 226 zoneItem.setContent(content); 227 zoneItem.setViewName(viewName); 228 229 return zoneItem; 230 } 231 232 /** 233 * Remove a zone item from page 234 * @param zoneItemId The id of zone item to delete 235 */ 236 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 237 public void removeZoneItem (String zoneItemId) 238 { 239 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 240 241 if (!(zoneItem instanceof ModifiableZoneItem)) 242 { 243 throw new IllegalArgumentException("Can not remove a non-modifiable zone item " + zoneItemId); 244 } 245 246 ModifiableSitemapElement sitemapElement = (ModifiableSitemapElement) zoneItem.getZone().getSitemapElement(); 247 if (_rightManager.hasRight(_currentUserProvider.getUser(), "Web_Rights_Page_DeleteZoneItem", sitemapElement) != RightResult.RIGHT_ALLOW) 248 { 249 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + " try to remove zone item without sufficient right"); 250 } 251 252 if (sitemapElement instanceof LockablePage lockablePage && lockablePage.isLocked()) 253 { 254 throw new IllegalArgumentException("Can not remove a zone item from a locked page " + lockablePage.getId()); 255 } 256 257 Map<String, Object> eventParams = new HashMap<>(); 258 eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement); 259 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItemId); 260 eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, zoneItem.getType()); 261 if (zoneItem.getType() == ZoneType.CONTENT) 262 { 263 // In case of a zone item of type content, provide it. 264 // There should be no problem as the observers are processed synchronously and, 265 // if the user want it to be deleted, it will happen in a future client call. 266 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_CONTENT, zoneItem.getContent()); 267 } 268 else if (zoneItem.getType() == ZoneType.SERVICE) 269 { 270 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_SERVICE, zoneItem.getServiceId()); 271 } 272 273 ((ModifiableZoneItem) zoneItem).remove(); 274 sitemapElement.saveChanges(); 275 276 _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_DELETED, _currentUserProvider.getUser(), eventParams)); 277 } 278 279 /** 280 * Get the unreferenced contents of a {@link Page} or a {@link ZoneItem} 281 * @param id The id of page or zone item 282 * @return The list of unreferenced contents 283 */ 284 @Callable(rights = Callable.NO_CHECK_REQUIRED) // no sensitive information 285 public Map<String, Object> getZoneItemElementInfo(String id) 286 { 287 ZoneItem zoneItem = _resolver.resolveById(id); 288 289 Map<String, Object> info = new HashMap<>(); 290 info.put("type", zoneItem.getType().toString().toLowerCase()); 291 292 Content content = getContent(zoneItem); 293 if (content != null) 294 { 295 info.put("id", content.getId()); 296 info.put("name", content.getName()); 297 info.put("title", content.getTitle(LocaleUtils.toLocale(zoneItem.getZone().getSitemapElement().getSitemapName()))); 298 info.put("isNew", _isNew(content)); 299 info.put("isShared", content instanceof SharedContent); 300 info.put("hasShared", _sharedContentManager.hasSharedContents(content)); 301 info.put("isReferenced", _isReferenced(content)); 302 } 303 else 304 { 305 Service service = getService(zoneItem); 306 if (service != null) 307 { 308 info.put("id", service.getId()); 309 info.put("label", service.getLabel()); 310 info.put("url", service.getURL()); 311 } 312 } 313 314 return info; 315 } 316 317 /** 318 * Get the content of a zone item. Can be null if zone item is a service zone item. 319 * @param zoneItem The zone item 320 * @return The content or null if zone item is a service zone item. 321 */ 322 public Content getContent (ZoneItem zoneItem) 323 { 324 if (zoneItem.getType() == ZoneItem.ZoneType.CONTENT) 325 { 326 return zoneItem.getContent(); 327 } 328 return null; 329 } 330 331 private boolean _isReferenced (Content content) 332 { 333 return content instanceof WebContent && ((WebContent) content).getReferencingPages().size() > 1; 334 } 335 336 private boolean _isNew (Content content) 337 { 338 boolean isNew = false; 339 if (content instanceof WorkflowAwareContent) 340 { 341 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 342 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 343 344 long workflowId = waContent.getWorkflowId(); 345 isNew = workflow.getHistorySteps(workflowId).isEmpty(); 346 } 347 return isNew; 348 } 349 350 /** 351 * Get the service of a zone item. Can be null if zone item is not a service zone item. 352 * @param zoneItem The zone item 353 * @return The service or null if zone item is not a service zone item. 354 */ 355 public Service getService (ZoneItem zoneItem) 356 { 357 if (zoneItem.getType() == ZoneItem.ZoneType.SERVICE) 358 { 359 return _serviceEP.getExtension(zoneItem.getServiceId()); 360 } 361 return null; 362 } 363 364 /** 365 * Move a zone item of a page before/after another zone item of the same page 366 * @param zoneItemId The identifier of the zone item to move 367 * @param zoneName The destination zone name 368 * @param beforeOrAfter true if before or false after 369 * @param beforeOrAfterItemId The target zone item can be null 370 * @param pageId The concerned page id 371 * @return true when success 372 * @throws UnknownAmetysObjectException If an error occurred 373 * @throws AmetysRepositoryException If an error occurred 374 * @throws RepositoryException If an error occurred 375 */ 376 @Callable (rights = "Web_Rights_Page_OrganizeZoneItem", paramIndex = 4, rightContext = PageRightAssignmentContext.ID) 377 public boolean moveZoneItemTo(String zoneItemId, String zoneName, boolean beforeOrAfter, String beforeOrAfterItemId, String pageId) throws UnknownAmetysObjectException, AmetysRepositoryException, RepositoryException 378 { 379 UserIdentity user = _currentUserProvider.getUser(); 380 381 ModifiableSitemapElement sitemapElement = _resolver.resolveById(pageId); 382 383 Zone zone; 384 if (!sitemapElement.hasZone(zoneName)) 385 { 386 zone = sitemapElement.createZone(zoneName); 387 } 388 else 389 { 390 zone = sitemapElement.getZone(zoneName); 391 } 392 393 ZoneItem zoneItem = sitemapElement instanceof JCRAmetysObject ? (ZoneItem) _resolver.resolveById(zoneItemId, ((JCRAmetysObject) sitemapElement).getNode().getSession()) : (ZoneItem) _resolver.resolveById(zoneItemId); 394 395 _checkZoneItem(sitemapElement, zoneName, zoneItem, user); 396 397 _checkArguments(zoneItemId, zoneName, pageId, user, sitemapElement, zone, zoneItem); 398 399 ((ModifiableZoneItem) zoneItem).moveTo(zone, false); 400 401 // For the moment the zoneItem is at the end 402 if (StringUtils.isNotBlank(beforeOrAfterItemId)) 403 { 404 if (beforeOrAfter) 405 { 406 ZoneItem futureFollowingZoneItem = _resolver.resolveById(beforeOrAfterItemId); 407 ((ModifiableZoneItem) zoneItem).orderBefore(futureFollowingZoneItem); 408 } 409 else 410 { 411 boolean isNext = false; 412 for (ZoneItem zi : zone.getZoneItems()) 413 { 414 if (isNext) 415 { 416 ((ModifiableZoneItem) zoneItem).orderBefore(zi); 417 break; 418 } 419 else if (beforeOrAfterItemId.equals(zi.getId())) 420 { 421 isNext = true; 422 } 423 } 424 } 425 } 426 427 ModifiableSitemapElement zoneItemPage = (ModifiableSitemapElement) zoneItem.getZone().getSitemapElement(); 428 if (zoneItemPage.needsSave()) 429 { 430 zoneItemPage.saveChanges(); 431 } 432 433 Map<String, Object> eventParams = new HashMap<>(); 434 eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement); 435 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItemId); 436 eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, zoneItem.getType()); 437 _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_MOVED, user, eventParams)); 438 439 return true; 440 } 441 442 /** 443 * Check if the zone item can be moved to the given zone of the given page. 444 * @param page the page. 445 * @param zoneName the zone name. 446 * @param zoneItem the zone item to move. 447 * @param user the user 448 */ 449 protected void _checkZoneItem(ModifiableSitemapElement page, String zoneName, ZoneItem zoneItem, UserIdentity user) 450 { 451 if (ZoneType.CONTENT.equals(zoneItem.getType())) 452 { 453 String[] contentTypes = zoneItem.getContent().getTypes(); 454 455 Set<String> availableCTypes = _cTypesAssignmentHandler.getAvailableContentTypes(page, zoneName, true); 456 457 for (String contentType : contentTypes) 458 { 459 if (!availableCTypes.contains(contentType)) 460 { 461 throw new IllegalArgumentException("The user '" + user + " illegally tried to move a content of type '" + contentType + "' to the zone '" + zoneName + "' of page '" + page.getId() + "'."); 462 } 463 } 464 } 465 else if (ZoneType.SERVICE.equals(zoneItem.getType())) 466 { 467 String serviceId = zoneItem.getServiceId(); 468 469 Set<String> services = _serviceAssignmentHandler.getAvailableServices(page, zoneName); 470 if (!services.contains(serviceId)) 471 { 472 throw new IllegalArgumentException("The user '" + user + " illegally tried to move a service of id '" + serviceId + "' to the zone '" + zoneName + "' of page '" + page.getId() + "'."); 473 } 474 } 475 } 476 477 private void _checkArguments(String zoneItemId, String zoneName, String pageId, UserIdentity user, ModifiableSitemapElement page, Zone zone, ZoneItem zoneItem) 478 { 479 if (_rightManager.hasRight(user, "Web_Rights_Page_OrganizeZoneItem", page) != RightResult.RIGHT_ALLOW) 480 { 481 throw new IllegalArgumentException("User '" + user + "' tryed without convinient privileges to move zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' !"); 482 } 483 else if (zoneItem == null) 484 { 485 throw new IllegalArgumentException("User '" + user + "' tryed to move unexisting zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' !"); 486 } 487 else if (zone == null) 488 { 489 throw new IllegalArgumentException("User '" + user + "' tryed to move zone item '" + zoneItemId + "' to an unexisting zone '" + zoneName + "' of the page '" + pageId + "' !"); 490 } 491 else if (!(zone instanceof ModifiableZone)) 492 { 493 throw new IllegalArgumentException("User '" + user + "' tryed to move zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' but this zone is not modifiable !"); 494 } 495 else if (!(zoneItem instanceof ModifiableZoneItem)) 496 { 497 throw new IllegalArgumentException("User '" + user + "' tryed to move zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' but this zone item is not modifiable !"); 498 } 499 } 500}