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 org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.cocoon.servlet.multipart.Part; 034import org.apache.commons.lang.StringUtils; 035import org.apache.excalibur.xml.sax.SAXParser; 036import org.apache.solr.client.solrj.SolrServerException; 037import org.xml.sax.InputSource; 038 039import org.ametys.cms.ObservationConstants; 040import org.ametys.cms.content.RichTextHandler; 041import org.ametys.cms.content.indexing.solr.SolrIndexer; 042import org.ametys.cms.contenttype.ContentType; 043import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 044import org.ametys.cms.data.Binary; 045import org.ametys.cms.data.RichText; 046import org.ametys.cms.data.type.ResourceElementTypeHelper; 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.opensymphony.workflow.WorkflowException; 076 077import jakarta.mail.MessagingException; 078 079/** 080 * Helper for wall contents 081 * 082 */ 083public class WallContentManager extends AbstractLogEnabled implements Component, Serviceable 084{ 085 /** The Avalon role */ 086 public static final String ROLE = WallContentManager.class.getName(); 087 /** The tag for pin */ 088 public static final String WALL_CONTENT_PIN_TAG = "WORKSPACES_CONTENT_PINNED"; 089 090 private static final int __INITIAL_WORKFLOW_ACTION_ID = 1111; 091 private static final String __WORKFLOW_NAME = "wall-content"; 092 093 private FOContentCreationHelper _foContentHelper; 094 private ContentTypeExtensionPoint _cTypeEP; 095 private I18nUtils _i18nUtils; 096 private ContentDAO _contentDAO; 097 private ObservationManager _observationManager; 098 private RightManager _rightManager; 099 private AmetysObjectResolver _resolver; 100 private UserManager _userManager; 101 private CurrentUserProvider _currentUserProvider; 102 private ProjectManager _projectManager; 103 private SiteManager _siteManager; 104 private ServiceManager _smanager; 105 private UploadManager _uploadManager; 106 private SolrIndexer _solrIndexer; 107 108 public void service(ServiceManager manager) throws ServiceException 109 { 110 _smanager = manager; 111 _foContentHelper = (FOContentCreationHelper) manager.lookup(FOContentCreationHelper.ROLE); 112 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 113 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 114 _contentDAO = (ContentDAO) manager.lookup(ContentDAO.ROLE); 115 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 116 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 117 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 118 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 119 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 120 _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE); 121 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 122 _uploadManager = (UploadManager) manager.lookup(UploadManager.ROLE); 123 _solrIndexer = (SolrIndexer) manager.lookup(SolrIndexer.ROLE); 124 } 125 126 /** 127 * Create and publish a new wall content 128 * @param text the text 129 * @param part the file part for illustration. Can be null. 130 * @param siteName the site name 131 * @param lang the language 132 * @return the results 133 */ 134 @Callable 135 public Map<String, Object> publishContent(String text, Part part, String siteName, String lang) 136 { 137 Map<String, Object> results = new HashMap<>(); 138 139 try 140 { 141 ContentType cType = _cTypeEP.getExtension(WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID); 142 143 String contentTitle = _i18nUtils.translate(cType.getDefaultTitle(), lang); 144 145 Map<String, Object> userValues = new HashMap<>(); 146 userValues.put(Content.ATTRIBUTE_TITLE, contentTitle); 147 userValues.put("content", text); 148 userValues.put("comment", true); // active comments 149 150 if (part != null) 151 { 152 try (InputStream is = part.getInputStream()) 153 { 154 Upload upload = _uploadManager.storeUpload(_currentUserProvider.getUser(), part.getFileName(), is); 155 Binary fileValue = ResourceElementTypeHelper.binaryFromUpload(upload); 156 157 userValues.put("illustration", Map.of("image", fileValue)); 158 } 159 catch (IOException e) 160 { 161 getLogger().error("Failed to store uploaded wall content illustration", e); 162 } 163 } 164 165 try 166 { 167 results = _foContentHelper.createAndEditContent(__INITIAL_WORKFLOW_ACTION_ID, WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID, siteName, contentTitle, contentTitle, lang, userValues, __WORKFLOW_NAME, null); 168 169 Content content = (Content) results.get(Content.class.getName()); 170 _notifyContentCreation(content); 171 172 // remove Content from result 173 results.remove(Content.class.getName()); 174 175 results.put("success", true); 176 } 177 finally 178 { 179 _commitAllChanges(); 180 } 181 } 182 catch (AmetysRepositoryException | WorkflowException e) 183 { 184 results.put("success", false); 185 getLogger().error("Failed to create wall content for site {} and language {}", siteName, lang, e); 186 } 187 return results; 188 } 189 190 /** 191 * Commit all changes in solr 192 */ 193 protected void _commitAllChanges() 194 { 195 // Before trying to commit, be sure all the async observers of the current request are finished 196 for (Future future : _observationManager.getFuturesForRequest()) 197 { 198 try 199 { 200 future.get(); 201 } 202 catch (ExecutionException | InterruptedException e) 203 { 204 getLogger().info("An exception occured when calling #get() on Future result of an observer." , e); 205 } 206 } 207 208 // Force commit all uncommited changes 209 try 210 { 211 _solrIndexer.commit(); 212 } 213 catch (IOException | SolrServerException e) 214 { 215 getLogger().error("Impossible to commit changes", e); 216 } 217 } 218 219 private void _notifyContentCreation(Content content) 220 { 221 Map<String, Object> eventParams = new HashMap<>(); 222 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 223 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 224 225 _observationManager.notify(new Event(org.ametys.plugins.workspaces.ObservationConstants.EVENT_WALLCONTENT_ADDED, content.getCreator(), eventParams)); 226 } 227 228 /** 229 * Pin a wall content 230 * @param contentId the content id 231 * @param contextualParameters the contextual parameters 232 * @return the result 233 */ 234 @Callable 235 public Map<String, Object> pinContent(String contentId, Map<String, Object> contextualParameters) 236 { 237 return _pinOrUnpinContent(contentId, contextualParameters, TagMode.INSERT); 238 } 239 240 /** 241 * Unpin a wall content 242 * @param contentId the content id 243 * @param contextualParameters the contextual parameters 244 * @return the result 245 */ 246 @Callable 247 public Map<String, Object> unpinContent(String contentId, Map<String, Object> contextualParameters) 248 { 249 return _pinOrUnpinContent(contentId, contextualParameters, TagMode.REMOVE); 250 } 251 252 private Map<String, Object> _pinOrUnpinContent(String contentId, Map<String, Object> contextualParameters, TagMode mode) 253 { 254 try 255 { 256 Map<String, Object> result = _contentDAO.tag(Collections.singletonList(contentId), Collections.singletonList(WALL_CONTENT_PIN_TAG), mode.name(), contextualParameters); 257 return result; 258 } 259 finally 260 { 261 _commitAllChanges(); 262 } 263 } 264 265 /** 266 * Report content to webmasters (user with report notification right on wall contents) 267 * @param siteName the current site name 268 * @param contentId the id of content to report 269 * @return true if the content was successfully reported 270 */ 271 @Callable 272 public boolean reportContent(String siteName, String contentId) 273 { 274 Content content = _resolver.resolveById(contentId); 275 User reporter = _userManager.getUser(_currentUserProvider.getUser()); 276 Site site = _siteManager.getSite(siteName); 277 278 // Add the report to the content 279 _contentDAO.report(content); 280 281 // Send a mail to the allowed users 282 List<Project> projects = _projectManager.getProjectsForSite(site); 283 if (!projects.isEmpty()) 284 { 285 Project project = projects.get(0); 286 287 List<String> recipients = _getReportsRecipients(siteName); 288 if (!recipients.isEmpty()) 289 { 290 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 291 i18nParams.put("projectTitle", new I18nizableText(project.getTitle())); 292 i18nParams.put("siteTitle", new I18nizableText(site.getTitle())); 293 294 I18nizableText i18nSubject = new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_WALL_CONTENT_REPORTED_SUBJECT", i18nParams); 295 String subject = _i18nUtils.translate(i18nSubject, content.getLanguage()); 296 297 i18nParams.put("siteUrl", new I18nizableText(site.getUrl())); 298 i18nParams.put("reporter", new I18nizableText(reporter.getFullName())); 299 i18nParams.put("content", new I18nizableText(getExcerpt(content, 200))); 300 301 I18nizableText i18nBody = new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_WALL_CONTENT_REPORTED_BODY", i18nParams); 302 String body = _i18nUtils.translate(i18nBody, content.getLanguage()); 303 304 String from = site.getValue("site-mail-from"); 305 306 try 307 { 308 SendMailHelper.newMail() 309 .withSubject(subject) 310 .withTextBody(body) 311 .withSender(from) 312 .withRecipients(recipients) 313 .sendMail(); 314 315 return true; 316 } 317 catch (MessagingException | IOException 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.hasValue("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}