001/*
002 *  Copyright 2019 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.plugins.ugc.clientsideelement;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.cocoon.ProcessingException;
029import org.apache.commons.lang.StringUtils;
030
031import org.ametys.cms.ObservationConstants;
032import org.ametys.cms.repository.Content;
033import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
034import org.ametys.cms.workflow.ContentWorkflowHelper;
035import org.ametys.core.observation.Event;
036import org.ametys.core.observation.ObservationManager;
037import org.ametys.core.ui.Callable;
038import org.ametys.core.ui.StaticClientSideElement;
039import org.ametys.plugins.repository.AmetysObjectResolver;
040import org.ametys.plugins.repository.AmetysRepositoryException;
041import org.ametys.plugins.repository.ModifiableAmetysObject;
042import org.ametys.plugins.repository.RemovableAmetysObject;
043import org.ametys.plugins.repository.data.holder.ModifiableDataHolder;
044import org.ametys.plugins.repository.version.VersionableAmetysObject;
045import org.ametys.plugins.ugc.UGCConstants;
046import org.ametys.runtime.model.exception.UndefinedItemPathException;
047import org.ametys.web.repository.content.WebContent;
048import org.ametys.web.repository.content.jcr.DefaultWebContent;
049import org.ametys.web.repository.page.ModifiablePage;
050import org.ametys.web.repository.page.ModifiableZone;
051import org.ametys.web.repository.page.ModifiableZoneItem;
052import org.ametys.web.repository.page.Page;
053import org.ametys.web.repository.page.Page.PageType;
054import org.ametys.web.repository.page.PageDAO;
055import org.ametys.web.repository.page.ZoneItem.ZoneType;
056import org.ametys.web.repository.site.Site;
057import org.ametys.web.repository.site.SiteManager;
058import org.ametys.web.skin.Skin;
059import org.ametys.web.skin.SkinTemplate;
060import org.ametys.web.skin.SkinsManager;
061
062import com.opensymphony.workflow.WorkflowException;
063
064/**
065 * Client side element for UGC content moderation
066 *
067 */
068public class UGCContentModerationClientSideElement extends StaticClientSideElement
069{
070    /** The Ametys resolver */
071    protected AmetysObjectResolver _resolver;
072    /** The page DAO */
073    protected PageDAO _pageDAO;
074    /** The site manager */
075    protected SiteManager _siteManager;
076    /** The skins manager */
077    protected SkinsManager _skinsManager;
078    /** The observation manager */
079    protected ObservationManager _observationManager;
080    /** The content workflow helper */
081    protected ContentWorkflowHelper _contentWorkflowHelper;
082
083    @Override   
084    public void service(ServiceManager smanager) throws ServiceException
085    {
086        super.service(smanager);
087        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
088        _pageDAO = (PageDAO) smanager.lookup(PageDAO.ROLE);
089        _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE);
090        _skinsManager = (SkinsManager) smanager.lookup(SkinsManager.ROLE);
091        _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE);
092        _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE);
093    }
094    
095    /**
096     * Accept a UGC content
097     * @param contentIds The id of UGC contents
098     * @param targetContentType The id of target content type. Can be null or empty to not change content type
099     * @param targetWorkflowName The workflow name for contents to create
100     * @param initActionId The id of workflow init action
101     * @param mode The insertion mode ('new' to insert contents in a new page, 'affect' to insert contents on a existing page or 'none' to keep content as orphan).
102     * @param pageId The page id. Can be null for mode 'none'
103     * @return the result
104     * @throws ProcessingException if failed to transform UGC contents
105     */
106    @Callable
107    public Map<String, Object> acceptUGCContent(List<String> contentIds, String targetContentType, String targetWorkflowName, int initActionId, String mode, String pageId) throws ProcessingException
108    {
109        Map<String, Object> result = new HashMap<>();
110        
111        result.put("createdContents", new ArrayList<>());
112        result.put("deletedContents", new HashSet<>());
113        
114        for (String contentId : contentIds)
115        {
116            try
117            {
118                DefaultWebContent ugcContent = _resolver.resolveById(contentId);
119                
120                // Create the content
121                Content newContent = createContent(ugcContent, targetWorkflowName, initActionId, targetContentType);
122                
123                try
124                {
125                    // Copy data
126                    ugcContent.copyTo((ModifiableDataHolder) newContent);
127                }
128                catch (UndefinedItemPathException e)
129                {
130                    getLogger().warn("The target content type '{}' is not compatible with the source UGC content type '{}'", targetContentType, ugcContent.getTypes(), e);
131                    
132                    result.put("success", false);
133                    result.put("error", "invalid-content-type");
134                    return result;
135                }
136                
137                // Save changes
138                ((ModifiableAmetysObject) newContent).saveChanges();
139                
140                // Notify observers
141                Map<String, Object> contentEventParams = new HashMap<>();
142                contentEventParams.put(ObservationConstants.ARGS_CONTENT, newContent);
143                contentEventParams.put(ObservationConstants.ARGS_CONTENT_ID, newContent.getId());
144                contentEventParams.put(ObservationConstants.ARGS_CONTENT_NAME, newContent.getName());
145                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), contentEventParams));
146                
147                // Create a new version
148                ((VersionableAmetysObject) newContent).checkpoint();
149                
150                ModifiablePage page = null;
151                if ("new".equals(mode))
152                {
153                    // Create new page to insert content
154                    page = createPage (pageId, ugcContent);
155                }
156                else if ("affect".equals(mode))
157                {
158                    // Insert content on given page
159                    page = _resolver.resolveById(pageId);
160                }
161                
162                String zoneName = null;
163                if (page != null)
164                {
165                    zoneName = getZoneName (page);
166                    if (zoneName == null)
167                    {
168                        getLogger().warn("Selected page '{}' is not a container page: can not affect a content", pageId);
169                        
170                        result.put("success", false);
171                        result.put("error", "invalid-page");
172                        return result;
173                    }
174                    
175                    ModifiableZone zone = null;
176                    if (page.hasZone(zoneName))
177                    {
178                        zone = page.getZone(zoneName);
179                    }
180                    else
181                    {
182                        zone = page.createZone(zoneName);
183                    }
184                    
185                    ModifiableZoneItem zoneItem = zone.addZoneItem();
186                    zoneItem.setType(ZoneType.CONTENT);
187                    zoneItem.setContent(newContent);
188                    zoneItem.setViewName("main");
189                    
190                    page.saveChanges();
191                    
192                    Map<String, Object> pageEventParams = new HashMap<>();
193                    pageEventParams.put(org.ametys.web.ObservationConstants.ARGS_SITEMAP_ELEMENT, page);
194                    pageEventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItem.getId());
195                    pageEventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_TYPE, ZoneType.CONTENT);
196                    pageEventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_ITEM_CONTENT, newContent);
197                    _observationManager.notify(new Event(org.ametys.web.ObservationConstants.EVENT_ZONEITEM_ADDED, _currentUserProvider.getUser(), pageEventParams));
198                }
199                
200                Map<String, Object> contentInfo = new HashMap<>();
201                contentInfo.put("title", newContent.getTitle());
202                contentInfo.put("id", newContent.getId());
203                if (page != null)
204                {
205                    contentInfo.put("pageId", page.getId());
206                }
207                
208                @SuppressWarnings("unchecked")
209                List<Map<String, Object>> acceptedContents = (List<Map<String, Object>>) result.get("createdContents");
210                acceptedContents.add(contentInfo);
211                
212                // Notify observers
213                _observationManager.notify(new Event(org.ametys.plugins.ugc.observation.ObservationConstants.EVENT_UGC_CONTENT_ACCEPTED, _currentUserProvider.getUser(), contentEventParams));
214                
215                // Delete initial content
216                String ugcContentId = ugcContent.getId();
217                deleteContent(ugcContent);
218                @SuppressWarnings("unchecked")
219                Set<String> deletedContents = (Set<String>) result.get("deletedContents");
220                deletedContents.add(ugcContentId);
221            }
222            catch (WorkflowException | AmetysRepositoryException e)
223            {
224                getLogger().error("Unable to transform UGC content '" + contentId + "'", e);
225                throw new ProcessingException("Unable to transform UGC content '" + contentId + "'", e);
226            }
227        }
228        
229        result.put("success", true);
230        return result;
231    }
232    
233    /**
234     * Refuse UGC content
235     * @param contentIds The id of UGC contents to refuse
236     * @param comment The reject's comment
237     * @param withNotification True to notify UGC author of the refuse
238     * @return the result
239     * @throws ProcessingException if failed to transform UGC contents
240     */
241    @Callable
242    public Map<String, Object> refuseUGCContent(List<String> contentIds, String comment, boolean withNotification) throws ProcessingException
243    {
244        Map<String, Object> result = new HashMap<>();
245        result.put("deletedContents", new HashSet<>());
246        
247        for (String contentId : contentIds)
248        {
249            try
250            {
251                DefaultWebContent ugcContent = _resolver.resolveById(contentId);
252                
253                // Notify observers
254                Map<String, Object> eventParams = new HashMap<>();
255                eventParams.put(ObservationConstants.ARGS_CONTENT, ugcContent);
256                eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, ugcContent.getName());
257                eventParams.put(ObservationConstants.ARGS_CONTENT_ID, ugcContent.getId());
258                eventParams.put(org.ametys.plugins.ugc.observation.ObservationConstants.ARGS_UGC_REFUSE_NOTIFY, withNotification);
259                eventParams.put(org.ametys.plugins.ugc.observation.ObservationConstants.ARGS_UGC_REFUSE_COMMENT, comment);
260                
261                _observationManager.notify(new Event(org.ametys.plugins.ugc.observation.ObservationConstants.EVENT_UGC_CONTENT_REFUSED, _currentUserProvider.getUser(), eventParams));
262                
263                // Delete initial content
264                String ugcContentId = ugcContent.getId();
265                deleteContent(ugcContent);
266                @SuppressWarnings("unchecked")
267                Set<String> deletedContents = (Set<String>) result.get("deletedContents");
268                deletedContents.add(ugcContentId);
269            }
270            catch (AmetysRepositoryException e)
271            {
272                getLogger().error("Unable to refuse UGC content '" + contentId + "'", e);
273                throw new ProcessingException("Unable to refuse UGC content '" + contentId + "'", e);
274            }
275        }
276        
277        result.put("success", true);
278        return result;
279    }
280    
281    /**
282     * Create page under a parent page
283     * @param parentId the parent page id or empty for the sitemap root
284     * @param content the UGC content
285     * @return the new created page
286     */
287    protected ModifiablePage createPage (String parentId, DefaultWebContent content)
288    {
289        String realParentId = parentId;
290        if (StringUtils.isEmpty(parentId))
291        {
292            Site site = _siteManager.getSite(content.getSiteName());
293            realParentId = site.getSitemap(content.getLanguage()).getId();
294        }
295
296        Map<String, Object> result = _pageDAO.createPage(realParentId, content.getTitle(), "");
297        ModifiablePage page = _resolver.resolveById((String) result.get("id"));
298        
299        _pageDAO.setTemplate(Collections.singletonList(page.getId()), "page");
300        
301        return page;
302    }
303    
304    /**
305     * Get the name of zone where to insert content
306     * @param page The page
307     * @return the zone's name
308     */
309    protected String getZoneName (Page page)
310    {
311        if (page.getType() == PageType.CONTAINER)
312        {
313            String skinId = page.getSite().getSkinId();
314            Skin skin = _skinsManager.getSkin(skinId);
315            SkinTemplate template = skin.getTemplate(page.getTemplate());
316            
317            // Has a default zone ?
318            if (template.getZone("default") != null)
319            {
320                return "default";
321            }
322            else
323            {
324                return template.getZones().keySet().iterator().next();
325            }
326        }
327        return null;
328    }
329    
330    /**
331     * Create the content from the proposed content
332     * @param initialContent The initial content
333     * @param workflowName the workflow name
334     * @param actionId The init action id
335     * @param cTypeId the content type
336     * @return the created content
337     * @throws WorkflowException if failed to create content
338     */
339    protected Content createContent (Content initialContent, String workflowName, int actionId, String cTypeId) throws WorkflowException
340    {
341        Map<String, Object> inputs = new HashMap<>();
342        inputs.put("prevent-version-creation", "true");
343        
344        if (initialContent instanceof WebContent)
345        {
346            inputs.put(org.ametys.web.workflow.CreateContentFunction.SITE_KEY, ((WebContent) initialContent).getSiteName());
347        }
348        
349        Map<String, Object> result =  _contentWorkflowHelper.createContent(workflowName,
350                actionId, 
351                initialContent.getName(), 
352                initialContent.getTitle(), 
353                new String[] {cTypeId}, 
354                new String[] {UGCConstants.UGC_MIXIN_TYPE}, 
355                initialContent.getLanguage(), 
356                inputs);
357        
358        return (Content) result.get(AbstractContentWorkflowComponent.CONTENT_KEY);
359        
360    }
361    
362    /**
363     * Delete the content
364     * @param content the content to delete
365     */
366    protected void deleteContent(Content content)
367    {
368        Map<String, Object> eventParams = new HashMap<>();
369        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
370        eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName());
371        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
372        
373        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParams));
374        
375        RemovableAmetysObject removableContent = (RemovableAmetysObject) content;
376        ModifiableAmetysObject parent = removableContent.getParent();
377        
378        // Remove the content.
379        removableContent.remove();
380        
381        parent.saveChanges();
382        
383        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams));
384    }
385}