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.net.MalformedURLException;
021import java.text.DateFormat;
022import java.util.ArrayList;
023import java.util.Date;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
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.environment.ObjectModelHelper;
037import org.apache.cocoon.environment.Request;
038import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
039import org.apache.commons.lang.StringUtils;
040import org.apache.excalibur.source.Source;
041import org.apache.excalibur.source.SourceResolver;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045import org.ametys.cms.content.archive.ArchiveConstants;
046import org.ametys.cms.repository.Content;
047import org.ametys.cms.repository.ContentQueryHelper;
048import org.ametys.core.authentication.AuthenticateAction;
049import org.ametys.core.engine.BackgroundEngineHelper;
050import org.ametys.core.engine.BackgroundEnvironment;
051import org.ametys.core.right.RightManager;
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.repository.AmetysObjectIterable;
059import org.ametys.plugins.repository.AmetysObjectResolver;
060import org.ametys.plugins.repository.AmetysRepositoryException;
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 ArchiveContentsEngine implements Runnable
076{
077    /** The logger. */
078    protected Logger _logger = LoggerFactory.getLogger(ArchiveContentsEngine.class);
079
080    /** The avalon context. */
081    protected Context _context;
082
083    /** The service manager. */
084    protected ServiceManager _manager;
085
086    /** The server base URL. */
087    protected String _baseUrl;
088
089    /** Is the engine initialized ? */
090    protected boolean _initialized;
091
092    /** The cocoon environment context. */
093    protected org.apache.cocoon.environment.Context _environmentContext;
094
095    /** The ametys object resolver. */
096    protected AmetysObjectResolver _ametysResolver;
097
098    /** The avalon source resolver. */
099    protected SourceResolver _sourceResolver;
100    
101    /** The rights manager */
102    protected RightManager _rightManager;
103
104    /** The users manager. */
105    protected UserManager _userManager;
106    
107    /** The i18n utils. */
108    protected I18nUtils _i18nUtils;
109    
110    /** The content of "from" field in emails. */
111    protected String _mailFrom;
112
113    /** The sysadmin mail address, to which will be sent the report e-mail. */
114    protected String _sysadminMail;
115    
116    /** The user e-mail notification will be sent to users that have this at least one of this rights. */
117    protected Set<String> _archiveRights;
118    
119    /** The user notification mail body i18n key. */
120    protected String _userMailBody;
121    
122    /** The user notification mail subject i18n key. */
123    protected String _userMailSubject;
124    
125    /** The user notification error mail body i18n key. */
126    protected String _userErrorMailBody;
127    
128    /** The user notification error mail subject i18n key. */
129    protected String _userErrorMailSubject;
130    
131    /**
132     * Initialize the archive engine.
133     * 
134     * @param manager the avalon service manager.
135     * @param context the avalon context.
136     * @throws ContextException If an error occurred
137     * @throws ServiceException If an error occurred
138     */
139    public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException
140    {
141        _manager = manager;
142        _context = context;
143        _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
144
145        // Lookup the needed components.
146        _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
147        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
148        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
149        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
150
151        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
152
153        _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/");
154        _mailFrom = Config.getInstance().getValue("smtp.mail.from");
155        _sysadminMail = Config.getInstance().getValue("smtp.mail.sysadminto");
156
157        _initialized = true;
158    }
159
160    /**
161     * Configure the engine (called by the scheduler).
162     * 
163     * @param configuration the component configuration.
164     * @throws ConfigurationException If an error occurred
165     */
166    public void configure(Configuration configuration) throws ConfigurationException
167    {
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    /**
195     * Check the initialization and throw an exception if not initialized.
196     */
197    protected void checkInitialization()
198    {
199        if (!_initialized)
200        {
201            String message = "The engine must be initialized before it can be runned.";
202            _logger.error(message);
203            throw new IllegalStateException(message);
204        }
205    }
206
207    @Override
208    public void run()
209    {
210        Map<String, Object> environmentInformation = null;
211        long duration = 0;
212        try
213        {
214            _logger.info("Preparing the scheduled archiving process on contents...");
215
216            checkInitialization();
217
218            // Create the environment.
219            environmentInformation = BackgroundEngineHelper.createAndEnterEngineEnvironment(_manager, _environmentContext, new SLF4JLoggerAdapter(_logger));
220            BackgroundEnvironment environment = (BackgroundEnvironment) environmentInformation.get("environment");
221
222            // Authorize workflow actions and "check-auth" CMS action, from this background environment 
223            Request request = (Request) environment.getObjectModel().get(ObjectModelHelper.REQUEST_OBJECT);
224            request.setAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INTERNAL_ALLOWED, true);
225
226            long start = System.currentTimeMillis();
227
228            // Get all the needed contents and archive them.
229            archiveContents(request);
230
231            long end = System.currentTimeMillis();
232
233            duration = (end - start) / 1000;
234        }
235        catch (Exception e)
236        {
237            _logger.error("An error occurred while archiving the contents.", e);
238        }
239        finally
240        {
241            // Leave the environment.
242            if (environmentInformation != null)
243            {
244                BackgroundEngineHelper.leaveEngineEnvironment(environmentInformation);
245            }
246
247            // Dispose of the resources.
248            dispose();
249            _logger.info("Scheduled archiving process ended after " + duration + " seconds.");
250        }
251    }
252
253    /**
254     * Dispose of the resources and looked-up components.
255     */
256    protected void dispose()
257    {
258        // Release the components.
259        if (_manager != null)
260        {
261            _manager.release(_ametysResolver);
262        }
263
264        _ametysResolver = null;
265        _sourceResolver = null;
266        _userManager = null;
267        
268        _i18nUtils = null;
269
270        _environmentContext = null;
271        _context = null;
272        _manager = null;
273
274        _initialized = false;
275    }
276
277    /**
278     * Get the contents that need to be archived, and archive them.
279     * @param request The current request object
280     * 
281     * @throws AmetysRepositoryException if an error occurs.
282     * @throws IOException If an error occurred
283     * @throws MalformedURLException If an error occurred
284     */
285    protected void archiveContents(Request request) throws AmetysRepositoryException, MalformedURLException, IOException
286    {
287        // Get all the content which scheduled archiving date is passed.
288        Expression dateExpression = new DateExpression(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE, Operator.LE, new Date(), ExpressionContext.newInstance().withUnversioned(true));
289        String query = ContentQueryHelper.getContentXPathQuery(dateExpression);
290
291        try (AmetysObjectIterable<Content> contents = _ametysResolver.query(query))
292        {
293            List<Content> archivedContents = new ArrayList<>();
294            List<Content> contentsWithError = new ArrayList<>();
295           
296            try
297            {
298                for (Content content : contents)
299                {
300                    setRequestAttributes(request, content);
301                    
302                    Set<UserIdentity> users = _getAuthorizedContributors(content);
303                    
304                    Source source = null;
305                    try
306                    {
307                        String contentId = content.getId();
308                        
309                        String uri = getArchiveActionUri(contentId);
310                        source = _sourceResolver.resolveURI(uri);
311                        
312                        try (InputStream is =  source.getInputStream())
313                        {
314                            String workspaceBackup = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
315                            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, ArchiveConstants.ARCHIVE_WORKSPACE);
316                            
317                            Content archivedContent = _ametysResolver.resolveById(contentId);
318                            archivedContents.add(archivedContent);
319                            
320                            sendMailToContributors(archivedContent, users);
321                            
322                            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceBackup);
323                        }
324                    }
325                    catch (Exception e)
326                    {
327                        contentsWithError.add (content);
328                        sendErrorMailToContributors(content, users);
329                        _logger.error("Error while trying to archive the content : " + content.getId() + "\nThis content is probably not archived.", e);
330                    }
331                    finally
332                    {
333                        if (source != null)
334                        {
335                            _sourceResolver.release(source);
336                        }
337                    }
338                }
339            }
340            finally
341            {
342                // Send mail to administrator
343                sendMailToAdministrator(archivedContents, contentsWithError);
344            }
345        }
346    }
347    
348    /**
349     * Set the necessary request attributes 
350     * @param request The request
351     * @param content The content
352     */
353    protected void setRequestAttributes (Request request, Content content)
354    {
355        // Set the population contexts to be able to get allowed users
356        List<String> populationContexts = new ArrayList<>();
357        populationContexts.add("/application");
358        
359        request.setAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR, populationContexts);
360    }
361    
362    /**
363     * Get the pipeline uri for the archive action
364     * @param contentId the current contend id
365     * @return a pipeline uri
366     */
367    protected String getArchiveActionUri(String contentId)
368    {
369        return "cocoon://_plugins/cms/archives/archive/" + ArchiveConstants.ARCHIVE_WORKFLOW_ACTION_ID + "?contentId=" + contentId;
370    }
371
372    /**
373     * Send the archive report e-mail.
374     * @param archivedContents The list of archived contents
375     * @param contentsWithError The list of contents with error
376     */
377    protected void sendMailToAdministrator(List<Content> archivedContents, List<Content> contentsWithError)
378    {
379        if (archivedContents.size() != 0 || contentsWithError.size() != 0)
380        {
381            try
382            {
383                I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_ARCHIVE_CONTENTS_REPORT_SUBJECT");
384                
385                List<String> bodyParams = getAdminEmailParams(archivedContents, contentsWithError);
386                I18nizableText i18nBody = new I18nizableText("plugin.cms", "PLUGINS_CMS_ARCHIVE_CONTENTS_REPORT_BODY", bodyParams);
387                
388                String subject = _i18nUtils.translate(i18nSubject);
389                String body = _i18nUtils.translate(i18nBody);
390                
391                if (StringUtils.isNotBlank(_sysadminMail))
392                {
393                    SendMailHelper.newMail()
394                                  .withSubject(subject)
395                                  .withTextBody(body)
396                                  .withSender(_mailFrom)
397                                  .withRecipient(_sysadminMail)
398                                  .sendMail();
399                }
400            }
401            catch (MessagingException | IOException e)
402            {
403                _logger.warn("Error sending the archive report e-mail.", e);
404            }
405        }
406    }
407    
408
409    /**
410     * Get the report e-mail parameters.
411     * @param archivedContents The list of archived contents
412     * @param contentsWithError The list of contents with error
413     * @return the e-mail parameters.
414     */
415    protected List<String> getAdminEmailParams(List<Content> archivedContents, List<Content> contentsWithError)
416    {
417        List<String> params = new ArrayList<>();
418        
419        DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT);
420        params.add(df.format(new Date())); // {0}
421        
422        params.add(String.valueOf(archivedContents.size())); // {1}
423        params.add(_getContentsListAsString(archivedContents)); // {2}
424
425        params.add(String.valueOf(contentsWithError.size())); // {3}
426        params.add(_getContentsListAsString(contentsWithError)); // {4}
427        
428        params.add(_getRequestURI(null) + "/index.html"); // {5}
429
430        return params;
431    }
432    
433    /**
434     * Get the contents list as String
435     * @param contents The contents
436     * @return the list of contents as String
437     */
438    protected String _getContentsListAsString (List<Content> contents)
439    {
440        List<String> contentNames = new ArrayList<>();
441        for (Content content : contents)
442        {
443            contentNames.add("- " + content.getTitle(null));
444        }
445        
446        if (contentNames.size() > 0)
447        {
448            return StringUtils.join(contentNames, "\n");
449        }
450        else
451        {
452            return "";
453        }
454    }
455    
456    /**
457     * Get the authorized contributors to receive mail notification
458     * @param content The content to be archived
459     * @return The user logins
460     */
461    protected Set<UserIdentity> _getAuthorizedContributors (Content content)
462    {
463        Set<UserIdentity> users = new HashSet<>();
464        for (String right : _archiveRights)
465        {
466            users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
467        }
468        
469        return users;
470    }
471
472    /**
473     * Send the mail to alert users that the content has been archived.
474     * @param content The archived content 
475     * @param users The users
476     */
477    protected void sendMailToContributors(Content content, Set<UserIdentity> users)
478    {
479        try
480        {
481            List<String> params = getBodyParamsForContributors (content, true);
482            
483            I18nizableText i18nSubject = new I18nizableText(null, _userMailSubject, params);
484            I18nizableText i18nBody = new I18nizableText(null, _userMailBody, params);
485            
486            String subject = _i18nUtils.translate(i18nSubject);
487            String body = _i18nUtils.translate(i18nBody);
488            
489            _sendMailsToUsers(subject, body, users, _mailFrom);
490        }
491        catch (Exception e)
492        {
493            _logger.error("Unable to send mail to contributors after archiving content '" + content.getName() + "'", e);
494        }
495    }
496    
497    /**
498     * Get email body parameters
499     * @param content the archived content
500     * @param archived true if the content has archived
501     * @return The mail parameters
502     */
503    protected List<String> getBodyParamsForContributors (Content content, boolean archived)
504    {
505        List<String> params = new ArrayList<>();
506        
507        params.add(content.getTitle(null)); // {0}
508        params.add(DateFormat.getDateInstance(DateFormat.LONG).format(new Date())); // {1}
509        
510        if (archived)
511        {
512            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}
513        }
514        else
515        {
516            params.add(_getRequestURI(content) + "/index.html?uitool=uitool-content,id:%27" + content.getId() + "%27");
517        }
518        
519        return params;
520    }
521    
522    /**
523     * Send the mail to alert users that an error has occurred while trying to archive the content.
524     * @param content The content 
525     * @param users The users
526     */
527    protected void sendErrorMailToContributors(Content content, Set<UserIdentity> users)
528    {
529        try
530        {
531            List<String> params = getBodyParamsForContributors (content, false);
532            
533            I18nizableText i18nSubject = new I18nizableText(null, _userErrorMailSubject, params);
534            I18nizableText i18nBody = new I18nizableText(null, _userErrorMailBody, params);
535            
536            String subject = _i18nUtils.translate(i18nSubject);
537            String body = _i18nUtils.translate(i18nBody);
538            
539            _sendMailsToUsers(subject, body, users, _mailFrom);
540        }
541        catch (Exception e)
542        {
543            _logger.error("Unable to send mail to contributors after archiving content '" + content.getName() + "' failed", e);
544        }
545    }
546    
547    /**
548     * Get the request URI to set in mail
549     * @param content The content. Can be null
550     * @return the request URI
551     */
552    protected String _getRequestURI (Content content)
553    {
554        return _baseUrl;
555    }
556
557    /**
558     * Send the emails to users (contributors)
559     * @param subject the e-mail subject.
560     * @param body the e-mail body.
561     * @param users users to send the mail to.
562     * @param from the address sending the e-mail.
563     */
564    protected void _sendMailsToUsers(String subject, String body, Set<UserIdentity> users, String from)
565    {
566        for (UserIdentity userIdentity : users)
567        {
568            User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
569            
570            if (user != null && StringUtils.isNotBlank(user.getEmail()))
571            {
572                String mail = user.getEmail();
573                
574                try
575                {
576                    SendMailHelper.newMail()
577                                  .withSubject(subject)
578                                  .withTextBody(body)
579                                  .withSender(from)
580                                  .withRecipient(mail)
581                                  .sendMail();
582                }
583                catch (MessagingException | IOException e)
584                {
585                    if (_logger.isWarnEnabled())
586                    {
587                        _logger.warn("Could not send an archive notification e-mail to " + mail, e);
588                    }
589                }
590            }
591        }
592    }
593}