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}