001/*
002 *  Copyright 2012 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.cms.workflow.archive;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.text.DateFormat;
021import java.util.ArrayList;
022import java.util.Date;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.configuration.Configuration;
030import org.apache.avalon.framework.configuration.ConfigurationException;
031import org.apache.avalon.framework.context.Context;
032import org.apache.avalon.framework.context.ContextException;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.cocoon.Constants;
036import org.apache.cocoon.components.ContextHelper;
037import org.apache.cocoon.environment.Request;
038import org.apache.commons.lang.StringUtils;
039import org.apache.excalibur.source.Source;
040import org.apache.excalibur.source.SourceResolver;
041import org.quartz.JobExecutionContext;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045import org.ametys.cms.content.ContentHelper;
046import org.ametys.cms.content.archive.ArchiveConstants;
047import org.ametys.cms.repository.Content;
048import org.ametys.cms.repository.ContentQueryHelper;
049import org.ametys.core.authentication.AuthenticateAction;
050import org.ametys.core.model.type.ModelItemTypeExtensionPoint;
051import org.ametys.core.right.RightManager;
052import org.ametys.core.schedule.progression.ContainerProgressionTracker;
053import org.ametys.core.ui.mail.StandardMailBodyHelper;
054import org.ametys.core.user.User;
055import org.ametys.core.user.UserIdentity;
056import org.ametys.core.user.UserManager;
057import org.ametys.core.user.population.PopulationContextHelper;
058import org.ametys.core.util.I18nUtils;
059import org.ametys.core.util.mail.SendMailHelper;
060import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable;
061import org.ametys.plugins.repository.AmetysObjectIterable;
062import org.ametys.plugins.repository.AmetysObjectResolver;
063import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
064import org.ametys.plugins.repository.query.expression.DateExpression;
065import org.ametys.plugins.repository.query.expression.Expression;
066import org.ametys.plugins.repository.query.expression.Expression.Operator;
067import org.ametys.plugins.repository.query.expression.ExpressionContext;
068import org.ametys.runtime.config.Config;
069import org.ametys.runtime.i18n.I18nizableText;
070
071import jakarta.mail.MessagingException;
072
073/**
074 * Runnable engine that archive the contents that have an scheduled archiving
075 * date set before the current date.
076 */
077public class ArchiveContentsSchedulable extends AbstractStaticSchedulable
078{
079    /** The logger. */
080    protected Logger _logger = LoggerFactory.getLogger(ArchiveContentsSchedulable.class);
081
082    /** The service manager. */
083    protected ServiceManager _manager;
084
085    /** The server base URL. */
086    protected String _baseUrl;
087
088    /** The cocoon environment context. */
089    protected org.apache.cocoon.environment.Context _environmentContext;
090
091    /** The ametys object resolver. */
092    protected AmetysObjectResolver _ametysResolver;
093
094    /** The avalon source resolver. */
095    protected SourceResolver _sourceResolver;
096    
097    /** The rights manager */
098    protected RightManager _rightManager;
099    
100    /** The i18n utils. */
101    protected I18nUtils _i18nUtils;
102    
103    /** The content helper */
104    protected ContentHelper _contentHelper;
105    
106    /** The content of "from" field in emails. */
107    protected String _mailFrom;
108
109    /** The sysadmin mail address, to which will be sent the report e-mail. */
110    protected String _sysadminMail;
111    
112    /** The user e-mail notification will be sent to users that have this at least one of this rights. */
113    protected Set<String> _archiveRights;
114    
115    /** The user notification mail body i18n key. */
116    protected String _userMailBody;
117    
118    /** The user notification mail subject i18n key. */
119    protected String _userMailSubject;
120    
121    /** The user notification error mail body i18n key. */
122    protected String _userErrorMailBody;
123    
124    /** The user notification error mail subject i18n key. */
125    protected String _userErrorMailSubject;
126    
127
128    @Override
129    public void service(ServiceManager manager) throws ServiceException
130    {
131        super.service(manager);
132        _smanager = manager;
133        _schedulableParameterTypeExtensionPoint = (ModelItemTypeExtensionPoint) _smanager.lookup(ModelItemTypeExtensionPoint.ROLE_SCHEDULABLE);
134        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
135        
136        _manager = manager;
137        // Lookup the needed components.
138        _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
139        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
140        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
141        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
142        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
143        
144        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
145
146        _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/");
147        _mailFrom = Config.getInstance().getValue("smtp.mail.from");
148        _sysadminMail = Config.getInstance().getValue("smtp.mail.sysadminto");
149    }
150
151    @Override
152    public void contextualize(Context context) throws ContextException
153    {
154        super.contextualize(context);
155        _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
156    }
157
158    /**
159     * Configure the engine (called by the scheduler).
160     * 
161     * @param configuration the component configuration.
162     * @throws ConfigurationException If an error occurred
163     */
164    @Override
165    public void configure(Configuration configuration) throws ConfigurationException
166    {
167        super.configure(configuration);
168        // Rights
169        Configuration rightsConf = configuration.getChild("rights");
170        
171        _archiveRights = new HashSet<>();
172        
173        for (Configuration rightConf : rightsConf.getChildren("right"))
174        {
175            String right = rightConf.getValue("");
176            if (StringUtils.isNotBlank(right))
177            {
178                _archiveRights.add(right);
179            }
180        }
181        
182        // Mails
183        Configuration validMailsConf = configuration.getChild("mails").getChild("valid");
184        Configuration errorMailsConf = configuration.getChild("mails").getChild("error");
185        
186        _userMailBody = validMailsConf.getChild("bodyKey").getValue();
187        _userMailSubject = validMailsConf.getChild("subjectKey").getValue();
188        
189        _userErrorMailBody = errorMailsConf.getChild("bodyKey").getValue();
190        _userErrorMailSubject = errorMailsConf.getChild("subjectKey").getValue();
191
192    }
193
194    @Override
195    public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception
196    {
197        Request request = ContextHelper.getRequest(_context);
198        
199        // Get all the content which scheduled archiving date is passed.
200        Expression dateExpression = new DateExpression(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE, Operator.LE, new Date(), ExpressionContext.newInstance().withUnversioned(true));
201        String query = ContentQueryHelper.getContentXPathQuery(dateExpression);
202
203        try (AmetysObjectIterable<Content> contents = _ametysResolver.query(query))
204        {
205            List<Content> archivedContents = new ArrayList<>();
206            List<Content> contentsWithError = new ArrayList<>();
207           
208            try
209            {
210                // Authorize workflow actions and "check-auth" CMS action
211                request.setAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INTERNAL_ALLOWED, true);
212                
213                for (Content content : contents)
214                {
215                    setRequestAttributes(request, content);
216                    
217                    Set<UserIdentity> users = _getAuthorizedContributors(content);
218                    
219                    Source source = null;
220                    try
221                    {
222                        String contentId = content.getId();
223                        
224                        String uri = getArchiveActionUri(contentId);
225                        source = _sourceResolver.resolveURI(uri);
226                        
227                        try (InputStream is =  source.getInputStream())
228                        {
229                            String workspaceBackup = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
230                            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, ArchiveConstants.ARCHIVE_WORKSPACE);
231                            
232                            Content archivedContent = _ametysResolver.resolveById(contentId);
233                            archivedContents.add(archivedContent);
234                            
235                            sendMailToContributors(archivedContent, users);
236                            
237                            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceBackup);
238                        }
239                    }
240                    catch (Exception e)
241                    {
242                        contentsWithError.add (content);
243                        sendErrorMailToContributors(content, users);
244                        _logger.error("Error while trying to archive the content : " + content.getId() + "\nThis content is probably not archived.", e);
245                    }
246                    finally
247                    {
248                        if (source != null)
249                        {
250                            _sourceResolver.release(source);
251                        }
252                    }
253                }
254            }
255            finally
256            {
257                request.removeAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INTERNAL_ALLOWED);
258                
259                // Send mail to administrator
260                sendMailToAdministrator(archivedContents, contentsWithError);
261            }
262        }
263    }
264    
265    /**
266     * Set the necessary request attributes 
267     * @param request The request
268     * @param content The content
269     */
270    protected void setRequestAttributes (Request request, Content content)
271    {
272        // Set the population contexts to be able to get allowed users
273        List<String> populationContexts = new ArrayList<>();
274        populationContexts.add("/application");
275        
276        request.setAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR, populationContexts);
277    }
278    
279    /**
280     * Get the pipeline uri for the archive action
281     * @param contentId the current contend id
282     * @return a pipeline uri
283     */
284    protected String getArchiveActionUri(String contentId)
285    {
286        return "cocoon://_plugins/cms/archives/archive/" + ArchiveConstants.ARCHIVE_WORKFLOW_ACTION_ID + "?contentId=" + contentId;
287    }
288
289    /**
290     * Send the archive report e-mail.
291     * @param archivedContents The list of archived contents
292     * @param contentsWithError The list of contents with error
293     */
294    protected void sendMailToAdministrator(List<Content> archivedContents, List<Content> contentsWithError)
295    {
296        if (archivedContents.size() != 0 || contentsWithError.size() != 0)
297        {
298            try
299            {
300                I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_ARCHIVE_CONTENTS_REPORT_SUBJECT");
301                
302                List<String> bodyParams = getAdminEmailParams(archivedContents, contentsWithError);
303                I18nizableText i18nBody = new I18nizableText("plugin.cms", "PLUGINS_CMS_ARCHIVE_CONTENTS_REPORT_BODY", bodyParams);
304                
305                String subject = _i18nUtils.translate(i18nSubject);
306                
307                String body = StandardMailBodyHelper.newHTMLBody()
308                        .withTitle(i18nSubject)
309                        .withMessage(i18nBody)
310                        .withLink(_getRequestURI(null) + "/index.html", new I18nizableText("plugin.cms", "PLUGINS_CMS_ARCHIVE_CONTENTS_REPORT_BODY_LINK"))
311                        .build();
312                
313                if (StringUtils.isNotBlank(_sysadminMail))
314                {
315                    SendMailHelper.newMail()
316                                  .withSubject(subject)
317                                  .withHTMLBody(body)
318                                  .withSender(_mailFrom)
319                                  .withRecipient(_sysadminMail)
320                                  .sendMail();
321                }
322            }
323            catch (MessagingException | IOException e)
324            {
325                _logger.warn("Error sending the archive report e-mail.", e);
326            }
327        }
328    }
329    
330
331    /**
332     * Get the report e-mail parameters.
333     * @param archivedContents The list of archived contents
334     * @param contentsWithError The list of contents with error
335     * @return the e-mail parameters.
336     */
337    protected List<String> getAdminEmailParams(List<Content> archivedContents, List<Content> contentsWithError)
338    {
339        List<String> params = new ArrayList<>();
340        
341        DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT);
342        params.add(df.format(new Date())); // {0}
343        
344        params.add(String.valueOf(archivedContents.size())); // {1}
345        params.add(_getContentsListAsString(archivedContents)); // {2}
346
347        params.add(String.valueOf(contentsWithError.size())); // {3}
348        params.add(_getContentsListAsString(contentsWithError)); // {4}
349
350        return params;
351    }
352    
353    /**
354     * Get the contents list as String
355     * @param contents The contents
356     * @return the list of contents as String
357     */
358    protected String _getContentsListAsString (List<Content> contents)
359    {
360        List<String> contentNames = contents.stream()
361            .map(this::_getContentTitle)
362            .collect(Collectors.toList());
363        
364        if (!contentNames.isEmpty())
365        {
366            StringBuilder sb = new StringBuilder();
367            sb.append("<ul>");
368            contentNames.forEach(c -> sb.append("<li>").append(c).append("</li>"));
369            sb.append("</ul>");
370            
371            return sb.toString();
372        }
373        else
374        {
375            return "";
376        }
377    }
378    
379    /**
380     * Get content title for mail
381     * @param content the content
382     * @return the content title
383     */
384    protected String _getContentTitle(Content content)
385    {
386        return content.getTitle(null);
387    }
388    
389    /**
390     * Get the authorized contributors to receive mail notification
391     * @param content The content to be archived
392     * @return The user logins
393     */
394    protected Set<UserIdentity> _getAuthorizedContributors (Content content)
395    {
396        Set<UserIdentity> users = new HashSet<>();
397        for (String right : _archiveRights)
398        {
399            users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
400        }
401        
402        return users;
403    }
404
405    /**
406     * Send the mail to alert users that the content has been archived.
407     * @param content The archived content 
408     * @param users The users
409     */
410    protected void sendMailToContributors(Content content, Set<UserIdentity> users)
411    {
412        try
413        {
414            List<String> params = getBodyParamsForContributors (content, true);
415            
416            I18nizableText i18nSubject = new I18nizableText(null, _userMailSubject, params);
417            I18nizableText i18nBody = new I18nizableText(null, _userMailBody, params);
418            
419            String body = StandardMailBodyHelper.newHTMLBody()
420                .withTitle(i18nSubject)
421                .withMessage(i18nBody)
422                .withLink(getContentUri(content, true), new I18nizableText("plugin.cms", "CONTENT_ARCHIVE_MAIL_CONTENT_BODY_CONTENT_LINK"))
423                .build();
424            
425            String subject = _i18nUtils.translate(i18nSubject);
426            
427            _sendMailsToUsers(subject, body, users, _mailFrom);
428        }
429        catch (Exception e)
430        {
431            _logger.error("Unable to send mail to contributors after archiving content '" + content.getName() + "'", e);
432        }
433    }
434    
435    /**
436     * Get email body parameters
437     * @param content the archived content
438     * @param archived true if the content has archived
439     * @return The mail parameters
440     */
441    protected List<String> getBodyParamsForContributors (Content content, boolean archived)
442    {
443        List<String> params = new ArrayList<>();
444        
445        params.add(content.getTitle(null)); // {0}
446        params.add(DateFormat.getDateInstance(DateFormat.LONG).format(new Date())); // {1}
447        params.add(getContentUri(content, archived));
448        
449        return params;
450    }
451    
452    /**
453     * Get the content uri
454     * @param content the archived content
455     * @param archived true if the content has archived
456     * @return the content uri
457     */
458    protected String getContentUri(Content content, boolean archived)
459    {
460        String contentBOUrl = _contentHelper.getContentBOUrl(content, Map.of());
461        if (archived)
462        {
463            return contentBOUrl + ",workspace:%27archives%27,%27ignore-workflow%27:%27true%27,%27content-message-type%27:%27archived-content%27";
464        }
465        else
466        {
467            return contentBOUrl;
468        }
469    }
470    
471    /**
472     * Send the mail to alert users that an error has occurred while trying to archive the content.
473     * @param content The content 
474     * @param users The users
475     */
476    protected void sendErrorMailToContributors(Content content, Set<UserIdentity> users)
477    {
478        try
479        {
480            List<String> params = getBodyParamsForContributors (content, false);
481            
482            I18nizableText i18nSubject = new I18nizableText(null, _userErrorMailSubject, params);
483            I18nizableText i18nBody = new I18nizableText(null, _userErrorMailBody, params);
484            
485            String body = StandardMailBodyHelper.newHTMLBody()
486                    .withTitle(i18nSubject)
487                    .withMessage(i18nBody)
488                    .withLink(getContentUri(content, false), new I18nizableText("plugin.cms", "CONTENT_ARCHIVE_MAIL_CONTENT_BODY_CONTENT_LINK"))
489                    .build();
490            
491            String subject = _i18nUtils.translate(i18nSubject);
492            
493            _sendMailsToUsers(subject, body, users, _mailFrom);
494        }
495        catch (Exception e)
496        {
497            _logger.error("Unable to send mail to contributors after archiving content '" + content.getName() + "' failed", e);
498        }
499    }
500    
501    /**
502     * Get the request URI to set in mail
503     * @param content The content. Can be null
504     * @return the request URI
505     */
506    protected String _getRequestURI (Content content)
507    {
508        return _baseUrl;
509    }
510
511    /**
512     * Send the emails to users (contributors)
513     * @param subject the e-mail subject.
514     * @param htmlBody the e-mail body.
515     * @param users users to send the mail to.
516     * @param from the address sending the e-mail.
517     */
518    protected void _sendMailsToUsers(String subject, String htmlBody, Set<UserIdentity> users, String from)
519    {
520        for (UserIdentity userIdentity : users)
521        {
522            User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
523            
524            if (user != null && StringUtils.isNotBlank(user.getEmail()))
525            {
526                String mail = user.getEmail();
527                
528                try
529                {
530                    SendMailHelper.newMail()
531                                  .withSubject(subject)
532                                  .withHTMLBody(htmlBody)
533                                  .withSender(from)
534                                  .withRecipient(mail)
535                                  .sendMail();
536                }
537                catch (MessagingException | IOException e)
538                {
539                    if (_logger.isWarnEnabled())
540                    {
541                        _logger.warn("Could not send an archive notification e-mail to " + mail, e);
542                    }
543                }
544            }
545        }
546    }
547}