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