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