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}