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}