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.stream.Collectors;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.cocoon.servlet.multipart.Part;
033import org.apache.commons.lang.StringUtils;
034import org.apache.excalibur.xml.sax.SAXParser;
035import org.apache.solr.client.solrj.SolrServerException;
036import org.xml.sax.InputSource;
037
038import org.ametys.cms.ObservationConstants;
039import org.ametys.cms.content.RichTextHandler;
040import org.ametys.cms.content.indexing.solr.SolrIndexer;
041import org.ametys.cms.contenttype.ContentType;
042import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
043import org.ametys.cms.data.Binary;
044import org.ametys.cms.data.RichText;
045import org.ametys.cms.data.type.ResourceElementTypeHelper;
046import org.ametys.cms.indexing.IndexingObserver;
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.observation.ObservationManager.ObserverFuture;
053import org.ametys.core.right.RightManager;
054import org.ametys.core.ui.Callable;
055import org.ametys.core.ui.mail.StandardMailBodyHelper;
056import org.ametys.core.upload.Upload;
057import org.ametys.core.upload.UploadManager;
058import org.ametys.core.user.CurrentUserProvider;
059import org.ametys.core.user.User;
060import org.ametys.core.user.UserIdentity;
061import org.ametys.core.user.UserManager;
062import org.ametys.core.util.I18nUtils;
063import org.ametys.core.util.mail.SendMailHelper;
064import org.ametys.plugins.explorer.resources.ResourceCollection;
065import org.ametys.plugins.repository.AmetysObjectResolver;
066import org.ametys.plugins.repository.AmetysRepositoryException;
067import org.ametys.plugins.workspaces.WorkspacesConstants;
068import org.ametys.plugins.workspaces.project.ProjectManager;
069import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
070import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
071import org.ametys.plugins.workspaces.project.objects.Project;
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        List<ObserverFuture> futuresForRequest = _observationManager.getFuturesForRequest();
204        for (ObserverFuture observerFuture : futuresForRequest)
205        {
206            if (observerFuture.traits().contains(IndexingObserver.INDEXING_OBSERVER))
207            {
208                try
209                {
210                    observerFuture.future().get();
211                }
212                catch (ExecutionException | InterruptedException e)
213                {
214                    getLogger().info("An exception occured when calling #get() on Future result of an observer." , e);
215                }
216            }
217        }
218        
219        // Force commit all uncommited changes
220        try
221        {
222            _solrIndexer.commit();
223        }
224        catch (IOException | SolrServerException e)
225        {
226            getLogger().error("Impossible to commit changes", e);
227        }
228    }
229    
230    private void _notifyContentCreation(Content content)
231    {
232        Map<String, Object> eventParams = new HashMap<>();
233        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
234        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
235        
236        _observationManager.notify(new Event(org.ametys.plugins.workspaces.ObservationConstants.EVENT_WALLCONTENT_ADDED, content.getCreator(), eventParams));
237    }
238    
239    /**
240     * Pin a wall content
241     * @param contentId the content id
242     * @param contextualParameters the contextual parameters
243     * @return the result
244     */
245    @Callable
246    public Map<String, Object> pinContent(String contentId, Map<String, Object> contextualParameters)
247    {
248        return _pinOrUnpinContent(contentId, contextualParameters, TagMode.INSERT);
249    }
250    
251    /**
252     * Unpin a wall content
253     * @param contentId the content id
254     * @param contextualParameters the contextual parameters
255     * @return the result
256     */
257    @Callable
258    public Map<String, Object> unpinContent(String contentId, Map<String, Object> contextualParameters)
259    {
260        return _pinOrUnpinContent(contentId, contextualParameters, TagMode.REMOVE);
261    }
262    
263    private Map<String, Object> _pinOrUnpinContent(String contentId,  Map<String, Object> contextualParameters, TagMode mode)
264    {
265        try
266        {
267            Map<String, Object> result = _contentDAO.tag(Collections.singletonList(contentId), Collections.singletonList(WALL_CONTENT_PIN_TAG), mode, contextualParameters, true);
268            return result;
269        }
270        finally
271        {
272            _commitAllChanges();
273        }
274    }
275    
276    /**
277     * Report content to webmasters (user with report notification right on wall contents)
278     * @param siteName the current site name
279     * @param contentId the id of content to report
280     * @return true if the content was successfully reported
281     */
282    @Callable
283    public boolean reportContent(String siteName, String contentId)
284    {
285        Content content = _resolver.resolveById(contentId);
286        User reporter = _userManager.getUser(_currentUserProvider.getUser());
287        Site site = _siteManager.getSite(siteName);
288        
289        // Add the report to the content
290        _contentDAO.report(content);
291        
292        // Send a mail to the allowed users
293        List<Project> projects = _projectManager.getProjectsForSite(site);
294        if (!projects.isEmpty())
295        {
296            Project project = projects.get(0);
297            
298            List<String> recipients = _getReportsRecipients(project);
299            if (!recipients.isEmpty())
300            {
301                Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
302                i18nParams.put("projectTitle", new I18nizableText(project.getTitle()));
303                i18nParams.put("siteTitle", new I18nizableText(site.getTitle()));
304                
305                I18nizableText i18nSubject = new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_WALL_CONTENT_REPORTED_SUBJECT", i18nParams);
306                String subject = _i18nUtils.translate(i18nSubject, content.getLanguage());
307                
308                i18nParams.put("projectUrl", new I18nizableText(site.getUrl()));
309                i18nParams.put("reporter", new I18nizableText(reporter.getFullName()));
310                i18nParams.put("content", new I18nizableText(getExcerpt(content, 200)));
311                
312                String from = site.getValue("site-mail-from");
313                
314                try
315                {
316                    String htmlBody = StandardMailBodyHelper.newHTMLBody()
317                            .withTitle(i18nSubject)
318                            .withLanguage(content.getLanguage())
319                            .withMessage(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_WALL_CONTENT_REPORTED_BODY", i18nParams))
320                            .withDetails(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_WALL_CONTENT_REPORTED_BODY_EXTRACT"), getExcerpt(content, 200), false)
321                            .withLink(site.getUrl(), new I18nizableText("plugin.workspaces", "PROJECT_MAIL_NOTIFICATION_BODY_DEFAULT_BUTTON_TEXT"))
322                            .build();
323                    
324                    SendMailHelper.newMail()
325                                  .withSubject(subject)
326                                  .withHTMLBody(htmlBody)
327                                  .withSender(from)
328                                  .withRecipients(recipients)
329                                  .sendMail();
330
331                    return true;
332                }
333                catch (MessagingException | IOException e)
334                {
335                    getLogger().warn("Could not send a notification mail to {}", recipients, e);
336                }
337            }
338        }
339        
340        return false;
341    }
342    
343    /**
344     * Get the excerpt of content
345     * @param content the content
346     * @param maxLength the max length for content excerpt
347     * @return the excerpt
348     */
349    public String getExcerpt(Content content, int maxLength)
350    {
351        if (content.hasValue("content"))
352        {
353            RichText richText = content.getValue("content");
354            SAXParser saxParser = null;
355            try (InputStream is = richText.getInputStream())
356            {
357                RichTextHandler txtHandler = new RichTextHandler(maxLength);
358                saxParser = (SAXParser) _smanager.lookup(SAXParser.ROLE);
359                saxParser.parse(new InputSource(is), txtHandler);
360                
361                return txtHandler.getValue();
362            }
363            catch (Exception e)
364            {
365                getLogger().error("Cannot extract excerpt from content {}", content.getId(), e);
366            }
367            finally
368            {
369                _smanager.release(saxParser);
370            }
371        }
372     
373        return "";
374    }
375    
376    /**
377     * Retrieves the list of recipients for reports notification sending
378     * @param project the current project
379     * @return the list of recipients for reports notification sending
380     */
381    protected List<String> _getReportsRecipients(Project project)
382    {
383        WorkspaceModule module = _moduleEP.getModule(WallContentModule.WALLCONTENT_MODULE_ID);
384        ResourceCollection moduleRoot = module.getModuleRoot(project, false);
385        
386        Set<UserIdentity> users = _rightManager.getAllowedUsers("Plugins_Workspaces_Right_ReportNotification_WallContent", moduleRoot).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"));
387        
388        return users.stream()
389                .map(_userManager::getUser)
390                .map(user -> user.getEmail())
391                .filter(StringUtils::isNotBlank)
392                .collect(Collectors.toList());
393    }
394    
395    /**
396     * Add or remove a reaction on a content
397     * @param contentId The content id
398     * @param reactionName the reaction name (ex: LIKE)
399     * @param remove true to remove the reaction, false to add reaction
400     * @return the result with the current actors of this reaction
401     */
402    @Callable
403    public Map<String, Object> react(String contentId, String reactionName, boolean remove)
404    {
405        return _contentDAO.react(contentId, reactionName, remove);
406    }
407    
408}