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}