001/* 002 * Copyright 2018 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.workspaces.wall; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.util.Collections; 021import java.util.HashMap; 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.mail.MessagingException; 030 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.avalon.framework.service.Serviceable; 035import org.apache.cocoon.servlet.multipart.Part; 036import org.apache.commons.lang.StringUtils; 037import org.apache.excalibur.xml.sax.SAXParser; 038import org.apache.solr.client.solrj.SolrServerException; 039import org.xml.sax.InputSource; 040 041import org.ametys.cms.ObservationConstants; 042import org.ametys.cms.content.RichTextHandler; 043import org.ametys.cms.content.indexing.solr.SolrIndexer; 044import org.ametys.cms.contenttype.ContentType; 045import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 046import org.ametys.cms.data.RichText; 047import org.ametys.cms.repository.Content; 048import org.ametys.cms.repository.ContentDAO; 049import org.ametys.cms.repository.ContentDAO.TagMode; 050import org.ametys.core.observation.Event; 051import org.ametys.core.observation.ObservationManager; 052import org.ametys.core.right.RightManager; 053import org.ametys.core.ui.Callable; 054import org.ametys.core.upload.Upload; 055import org.ametys.core.upload.UploadManager; 056import org.ametys.core.user.CurrentUserProvider; 057import org.ametys.core.user.User; 058import org.ametys.core.user.UserIdentity; 059import org.ametys.core.user.UserManager; 060import org.ametys.core.util.I18nUtils; 061import org.ametys.core.util.mail.SendMailHelper; 062import org.ametys.plugins.repository.AmetysObjectResolver; 063import org.ametys.plugins.repository.AmetysRepositoryException; 064import org.ametys.plugins.workspaces.WorkspacesConstants; 065import org.ametys.plugins.workspaces.project.ProjectManager; 066import org.ametys.plugins.workspaces.project.objects.Project; 067import org.ametys.runtime.config.Config; 068import org.ametys.runtime.i18n.I18nizableText; 069import org.ametys.runtime.i18n.I18nizableTextParameter; 070import org.ametys.runtime.plugin.component.AbstractLogEnabled; 071import org.ametys.web.content.FOContentCreationHelper; 072import org.ametys.web.repository.site.Site; 073import org.ametys.web.repository.site.SiteManager; 074 075import com.google.common.collect.ArrayListMultimap; 076import com.opensymphony.workflow.WorkflowException; 077 078/** 079 * Helper for wall contents 080 * 081 */ 082public class WallContentManager extends AbstractLogEnabled implements Component, Serviceable 083{ 084 /** The Avalon role */ 085 public static final String ROLE = WallContentManager.class.getName(); 086 /** The tag for pin */ 087 public static final String WALL_CONTENT_PIN_TAG = "WORKSPACES_CONTENT_PINNED"; 088 089 private static final int __INITIAL_WORKFLOW_ACTION_ID = 1111; 090 private static final String __WORKFLOW_NAME = "wall-content"; 091 092 private FOContentCreationHelper _foContentHelper; 093 private ContentTypeExtensionPoint _cTypeEP; 094 private I18nUtils _i18nUtils; 095 private ContentDAO _contentDAO; 096 private ObservationManager _observationManager; 097 private RightManager _rightManager; 098 private AmetysObjectResolver _resolver; 099 private UserManager _userManager; 100 private CurrentUserProvider _currentUserProvider; 101 private ProjectManager _projectManager; 102 private SiteManager _siteManager; 103 private ServiceManager _smanager; 104 private UploadManager _uploadManager; 105 private SolrIndexer _solrIndexer; 106 107 public void service(ServiceManager manager) throws ServiceException 108 { 109 _smanager = manager; 110 _foContentHelper = (FOContentCreationHelper) manager.lookup(FOContentCreationHelper.ROLE); 111 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 112 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 113 _contentDAO = (ContentDAO) manager.lookup(ContentDAO.ROLE); 114 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 115 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 116 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 117 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 118 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 119 _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE); 120 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 121 _uploadManager = (UploadManager) manager.lookup(UploadManager.ROLE); 122 _solrIndexer = (SolrIndexer) manager.lookup(SolrIndexer.ROLE); 123 } 124 125 /** 126 * Create and publish a new wall content 127 * @param text the text 128 * @param part the file part for illustration. Can be null. 129 * @param siteName the site name 130 * @param lang the language 131 * @return the results 132 */ 133 @Callable 134 public Map<String, Object> publishContent(String text, Part part, String siteName, String lang) 135 { 136 Map<String, Object> results = new HashMap<>(); 137 138 try 139 { 140 ContentType cType = _cTypeEP.getExtension(WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID); 141 142 String contentTitle = _i18nUtils.translate(cType.getDefaultTitle(), lang); 143 144 Map<String, Object> rawValues = new HashMap<>(); 145 rawValues.put("title", contentTitle); 146 rawValues.put("content", text); 147 rawValues.put("comment", true); // active comments 148 149 if (part != null) 150 { 151 try (InputStream is = part.getInputStream()) 152 { 153 Upload upload = _uploadManager.storeUpload(_currentUserProvider.getUser(), part.getFileName(), is); 154 155 Map<String, Object> fileValue = new HashMap<>(); 156 fileValue.put("id", upload.getId()); 157 fileValue.put("filename", upload.getFilename()); 158 fileValue.put("size", upload.getLength()); 159 fileValue.put("type", "attribute"); 160 161 rawValues.put("illustration/image", fileValue); 162 } 163 catch (IOException e) 164 { 165 getLogger().error("Failed to store uploaded wall content illustration", e); 166 } 167 } 168 169 try 170 { 171 Map<String, Object> userValues = _foContentHelper.getAndValidateFormValues(rawValues, cType, "main", ArrayListMultimap.create()); 172 173 results = _foContentHelper.createAndEditContent(__INITIAL_WORKFLOW_ACTION_ID, WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID, siteName, contentTitle, contentTitle, lang, userValues, __WORKFLOW_NAME, null); 174 175 Content content = (Content) results.get(Content.class.getName()); 176 _notifyContentCreation(content); 177 178 // remove Content from result 179 results.remove(Content.class.getName()); 180 181 results.put("success", true); 182 } 183 finally 184 { 185 _commitAllChanges(); 186 } 187 } 188 catch (AmetysRepositoryException | WorkflowException e) 189 { 190 results.put("success", false); 191 getLogger().error("Failed to create wall content for site {} and language {}", siteName, lang, e); 192 } 193 return results; 194 } 195 196 /** 197 * Commit all changes in solr 198 */ 199 protected void _commitAllChanges() 200 { 201 // Before trying to commit, be sure all the async observers of the current request are finished 202 for (Future future : _observationManager.getFuturesForRequest()) 203 { 204 try 205 { 206 future.get(); 207 } 208 catch (ExecutionException | InterruptedException e) 209 { 210 getLogger().info("An exception occured when calling #get() on Future result of an observer." , e); 211 } 212 } 213 214 // Force commit all uncommited changes 215 try 216 { 217 _solrIndexer.commit(); 218 } 219 catch (IOException | SolrServerException e) 220 { 221 getLogger().error("Impossible to commit changes", e); 222 } 223 } 224 225 private void _notifyContentCreation(Content content) 226 { 227 Map<String, Object> eventParams = new HashMap<>(); 228 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 229 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 230 231 _observationManager.notify(new Event(org.ametys.plugins.workspaces.ObservationConstants.EVENT_WALLCONTENT_ADDED, content.getCreator(), eventParams)); 232 } 233 234 /** 235 * Pin a wall content 236 * @param contentId the content id 237 * @param contextualParameters the contextual parameters 238 * @return the result 239 */ 240 @Callable 241 public Map<String, Object> pinContent(String contentId, Map<String, Object> contextualParameters) 242 { 243 return _pinOrUnpinContent(contentId, contextualParameters, TagMode.INSERT); 244 } 245 246 /** 247 * Unpin a wall content 248 * @param contentId the content id 249 * @param contextualParameters the contextual parameters 250 * @return the result 251 */ 252 @Callable 253 public Map<String, Object> unpinContent(String contentId, Map<String, Object> contextualParameters) 254 { 255 return _pinOrUnpinContent(contentId, contextualParameters, TagMode.REMOVE); 256 } 257 258 private Map<String, Object> _pinOrUnpinContent(String contentId, Map<String, Object> contextualParameters, TagMode mode) 259 { 260 try 261 { 262 Map<String, Object> result = _contentDAO.tag(Collections.singletonList(contentId), Collections.singletonList(WALL_CONTENT_PIN_TAG), mode.name(), contextualParameters); 263 return result; 264 } 265 finally 266 { 267 _commitAllChanges(); 268 } 269 } 270 271 /** 272 * Report content to webmasters (user with report notification right on wall contents) 273 * @param siteName the current site name 274 * @param contentId the id of content to report 275 * @return true if the content was successfully reported 276 */ 277 @Callable 278 public boolean reportContent(String siteName, String contentId) 279 { 280 Content content = _resolver.resolveById(contentId); 281 User reporter = _userManager.getUser(_currentUserProvider.getUser()); 282 Site site = _siteManager.getSite(siteName); 283 284 // Add the report to the content 285 _contentDAO.report(content); 286 287 // Send a mail to the allowed users 288 List<Project> projects = _projectManager.getProjectsForSite(site); 289 if (!projects.isEmpty()) 290 { 291 Project project = projects.get(0); 292 293 List<String> recipients = _getReportsRecipients(siteName); 294 if (!recipients.isEmpty()) 295 { 296 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 297 i18nParams.put("projectTitle", new I18nizableText(project.getTitle())); 298 i18nParams.put("siteTitle", new I18nizableText(site.getTitle())); 299 300 I18nizableText i18nSubject = new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_WALL_CONTENT_REPORTED_SUBJECT", i18nParams); 301 String subject = _i18nUtils.translate(i18nSubject, content.getLanguage()); 302 303 i18nParams.put("siteUrl", new I18nizableText(site.getUrl())); 304 i18nParams.put("reporter", new I18nizableText(reporter.getFullName())); 305 i18nParams.put("content", new I18nizableText(getExcerpt(content, 200))); 306 307 I18nizableText i18nBody = new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_WALL_CONTENT_REPORTED_BODY", i18nParams); 308 String body = _i18nUtils.translate(i18nBody, content.getLanguage()); 309 310 String from = site.getValue("site-mail-from"); 311 312 try 313 { 314 SendMailHelper.sendMail(subject, null, body, recipients, from, true); 315 return true; 316 } 317 catch (MessagingException e) 318 { 319 getLogger().warn("Could not send a notification mail to {}", recipients, e); 320 } 321 } 322 } 323 324 return false; 325 } 326 327 /** 328 * Get the excerpt of content 329 * @param content the content 330 * @param maxLength the max length for content excerpt 331 * @return the excerpt 332 */ 333 public String getExcerpt(Content content, int maxLength) 334 { 335 if (content.hasNonEmptyValue("content")) 336 { 337 RichText richText = content.getValue("content"); 338 SAXParser saxParser = null; 339 try (InputStream is = richText.getInputStream()) 340 { 341 RichTextHandler txtHandler = new RichTextHandler(maxLength); 342 saxParser = (SAXParser) _smanager.lookup(SAXParser.ROLE); 343 saxParser.parse(new InputSource(is), txtHandler); 344 345 return txtHandler.getValue(); 346 } 347 catch (Exception e) 348 { 349 getLogger().error("Cannot extract excerpt from content {}", content.getId(), e); 350 } 351 finally 352 { 353 _smanager.release(saxParser); 354 } 355 } 356 357 return ""; 358 } 359 360 /** 361 * Retrieves the list of recipients for reports notification sending 362 * @param siteName the current site name 363 * @return the list of recipients for reports notification sending 364 */ 365 protected List<String> _getReportsRecipients(String siteName) 366 { 367 Set<UserIdentity> users = _rightManager.getAllowedUsers("Plugins_Workspaces_Right_ReportNotification_WallContent", "/cms/" + siteName).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")); 368 369 return users.stream() 370 .map(_userManager::getUser) 371 .map(user -> user.getEmail()) 372 .filter(StringUtils::isNotBlank) 373 .collect(Collectors.toList()); 374 } 375 376 /** 377 * Add or remove a reaction on a content 378 * @param contentId The content id 379 * @param reactionName the reaction name (ex: LIKE) 380 * @param remove true to remove the reaction, false to add reaction 381 * @return the result with the current actors of this reaction 382 */ 383 @Callable 384 public Map<String, Object> react(String contentId, String reactionName, boolean remove) 385 { 386 return _contentDAO.react(contentId, reactionName, remove); 387 } 388 389}