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