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