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.ui.mail.StandardMailBodyHelper; 055import org.ametys.core.upload.Upload; 056import org.ametys.core.upload.UploadManager; 057import org.ametys.core.user.CurrentUserProvider; 058import org.ametys.core.user.User; 059import org.ametys.core.user.UserIdentity; 060import org.ametys.core.user.UserManager; 061import org.ametys.core.util.I18nUtils; 062import org.ametys.core.util.mail.SendMailHelper; 063import org.ametys.plugins.explorer.resources.ResourceCollection; 064import org.ametys.plugins.repository.AmetysObjectResolver; 065import org.ametys.plugins.repository.AmetysRepositoryException; 066import org.ametys.plugins.workspaces.WorkspacesConstants; 067import org.ametys.plugins.workspaces.project.ProjectManager; 068import org.ametys.plugins.workspaces.project.modules.WorkspaceModule; 069import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 070import org.ametys.plugins.workspaces.project.objects.Project; 071import org.ametys.runtime.config.Config; 072import org.ametys.runtime.i18n.I18nizableText; 073import org.ametys.runtime.i18n.I18nizableTextParameter; 074import org.ametys.runtime.plugin.component.AbstractLogEnabled; 075import org.ametys.web.content.FOContentCreationHelper; 076import org.ametys.web.repository.site.Site; 077import org.ametys.web.repository.site.SiteManager; 078 079import com.opensymphony.workflow.WorkflowException; 080 081import jakarta.mail.MessagingException; 082 083/** 084 * Helper for wall contents 085 * 086 */ 087public class WallContentManager extends AbstractLogEnabled implements Component, Serviceable 088{ 089 /** The Avalon role */ 090 public static final String ROLE = WallContentManager.class.getName(); 091 /** The tag for pin */ 092 public static final String WALL_CONTENT_PIN_TAG = "WORKSPACES_CONTENT_PINNED"; 093 094 private static final int __INITIAL_WORKFLOW_ACTION_ID = 1111; 095 private static final String __WORKFLOW_NAME = "wall-content"; 096 097 private FOContentCreationHelper _foContentHelper; 098 private ContentTypeExtensionPoint _cTypeEP; 099 private I18nUtils _i18nUtils; 100 private ContentDAO _contentDAO; 101 private ObservationManager _observationManager; 102 private RightManager _rightManager; 103 private AmetysObjectResolver _resolver; 104 private UserManager _userManager; 105 private CurrentUserProvider _currentUserProvider; 106 private ProjectManager _projectManager; 107 private SiteManager _siteManager; 108 private ServiceManager _smanager; 109 private UploadManager _uploadManager; 110 private SolrIndexer _solrIndexer; 111 private WorkspaceModuleExtensionPoint _moduleEP; 112 113 public void service(ServiceManager manager) throws ServiceException 114 { 115 _smanager = manager; 116 _foContentHelper = (FOContentCreationHelper) manager.lookup(FOContentCreationHelper.ROLE); 117 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 118 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 119 _contentDAO = (ContentDAO) manager.lookup(ContentDAO.ROLE); 120 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 121 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 122 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 123 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 124 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 125 _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE); 126 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 127 _uploadManager = (UploadManager) manager.lookup(UploadManager.ROLE); 128 _solrIndexer = (SolrIndexer) manager.lookup(SolrIndexer.ROLE); 129 _moduleEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE); 130 } 131 132 /** 133 * Create and publish a new wall content 134 * @param text the text 135 * @param part the file part for illustration. Can be null. 136 * @param siteName the site name 137 * @param lang the language 138 * @return the results 139 */ 140 @Callable 141 public Map<String, Object> publishContent(String text, Part part, String siteName, String lang) 142 { 143 Map<String, Object> results = new HashMap<>(); 144 145 try 146 { 147 ContentType cType = _cTypeEP.getExtension(WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID); 148 149 String contentTitle = _i18nUtils.translate(cType.getDefaultTitle(), lang); 150 151 Map<String, Object> userValues = new HashMap<>(); 152 userValues.put(Content.ATTRIBUTE_TITLE, contentTitle); 153 userValues.put("content", text); 154 userValues.put("comment", true); // active comments 155 156 if (part != null) 157 { 158 try (InputStream is = part.getInputStream()) 159 { 160 Upload upload = _uploadManager.storeUpload(_currentUserProvider.getUser(), part.getFileName(), is); 161 Binary fileValue = ResourceElementTypeHelper.binaryFromUpload(upload); 162 163 userValues.put("illustration", Map.of("image", fileValue)); 164 } 165 catch (IOException e) 166 { 167 getLogger().error("Failed to store uploaded wall content illustration", e); 168 } 169 } 170 171 try 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, contextualParameters, true); 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(project); 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("projectUrl", new I18nizableText(site.getUrl())); 304 i18nParams.put("reporter", new I18nizableText(reporter.getFullName())); 305 i18nParams.put("content", new I18nizableText(getExcerpt(content, 200))); 306 307 String from = site.getValue("site-mail-from"); 308 309 try 310 { 311 String htmlBody = StandardMailBodyHelper.newHTMLBody() 312 .withTitle(i18nSubject) 313 .withLanguage(content.getLanguage()) 314 .withMessage(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_WALL_CONTENT_REPORTED_BODY", i18nParams)) 315 .withDetails(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_WALL_CONTENT_REPORTED_BODY_EXTRACT"), getExcerpt(content, 200), false) 316 .withLink(site.getUrl(), new I18nizableText("plugin.workspaces", "PROJECT_MAIL_NOTIFICATION_BODY_DEFAULT_BUTTON_TEXT")) 317 .build(); 318 319 SendMailHelper.newMail() 320 .withSubject(subject) 321 .withHTMLBody(htmlBody) 322 .withSender(from) 323 .withRecipients(recipients) 324 .sendMail(); 325 326 return true; 327 } 328 catch (MessagingException | IOException e) 329 { 330 getLogger().warn("Could not send a notification mail to {}", recipients, e); 331 } 332 } 333 } 334 335 return false; 336 } 337 338 /** 339 * Get the excerpt of content 340 * @param content the content 341 * @param maxLength the max length for content excerpt 342 * @return the excerpt 343 */ 344 public String getExcerpt(Content content, int maxLength) 345 { 346 if (content.hasValue("content")) 347 { 348 RichText richText = content.getValue("content"); 349 SAXParser saxParser = null; 350 try (InputStream is = richText.getInputStream()) 351 { 352 RichTextHandler txtHandler = new RichTextHandler(maxLength); 353 saxParser = (SAXParser) _smanager.lookup(SAXParser.ROLE); 354 saxParser.parse(new InputSource(is), txtHandler); 355 356 return txtHandler.getValue(); 357 } 358 catch (Exception e) 359 { 360 getLogger().error("Cannot extract excerpt from content {}", content.getId(), e); 361 } 362 finally 363 { 364 _smanager.release(saxParser); 365 } 366 } 367 368 return ""; 369 } 370 371 /** 372 * Retrieves the list of recipients for reports notification sending 373 * @param project the current project 374 * @return the list of recipients for reports notification sending 375 */ 376 protected List<String> _getReportsRecipients(Project project) 377 { 378 WorkspaceModule module = _moduleEP.getModule(WallContentModule.WALLCONTENT_MODULE_ID); 379 ResourceCollection moduleRoot = module.getModuleRoot(project, false); 380 381 Set<UserIdentity> users = _rightManager.getAllowedUsers("Plugins_Workspaces_Right_ReportNotification_WallContent", moduleRoot).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")); 382 383 return users.stream() 384 .map(_userManager::getUser) 385 .map(user -> user.getEmail()) 386 .filter(StringUtils::isNotBlank) 387 .collect(Collectors.toList()); 388 } 389 390 /** 391 * Add or remove a reaction on a content 392 * @param contentId The content id 393 * @param reactionName the reaction name (ex: LIKE) 394 * @param remove true to remove the reaction, false to add reaction 395 * @return the result with the current actors of this reaction 396 */ 397 @Callable 398 public Map<String, Object> react(String contentId, String reactionName, boolean remove) 399 { 400 return _contentDAO.react(contentId, reactionName, remove); 401 } 402 403}