001/*
002 *  Copyright 2010 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.content.consistency;
017
018import java.io.ByteArrayOutputStream;
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.FileOutputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025import java.util.ArrayList;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030
031import javax.mail.MessagingException;
032
033import org.apache.avalon.framework.configuration.Configuration;
034import org.apache.avalon.framework.configuration.ConfigurationException;
035import org.apache.avalon.framework.context.Context;
036import org.apache.avalon.framework.context.ContextException;
037import org.apache.avalon.framework.service.ServiceException;
038import org.apache.avalon.framework.service.ServiceManager;
039import org.apache.cocoon.Constants;
040import org.apache.cocoon.components.ContextHelper;
041import org.apache.cocoon.components.source.impl.SitemapSource;
042import org.apache.cocoon.environment.ObjectModelHelper;
043import org.apache.cocoon.environment.Request;
044import org.apache.cocoon.environment.background.BackgroundEnvironment;
045import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
046import org.apache.commons.io.FileUtils;
047import org.apache.commons.lang.StringUtils;
048import org.apache.commons.lang.math.NumberUtils;
049import org.apache.excalibur.source.Source;
050import org.apache.excalibur.source.SourceResolver;
051import org.apache.excalibur.source.SourceUtil;
052import org.apache.excalibur.xml.sax.SAXParser;
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055import org.xml.sax.Attributes;
056import org.xml.sax.InputSource;
057import org.xml.sax.SAXException;
058import org.xml.sax.helpers.DefaultHandler;
059
060import org.ametys.core.authentication.AuthenticateAction;
061import org.ametys.core.engine.BackgroundEngineHelper;
062import org.ametys.core.right.RightManager;
063import org.ametys.core.user.User;
064import org.ametys.core.user.UserIdentity;
065import org.ametys.core.user.UserManager;
066import org.ametys.core.user.population.PopulationContextHelper;
067import org.ametys.core.util.I18nUtils;
068import org.ametys.core.util.mail.SendMailHelper;
069import org.ametys.plugins.repository.AmetysObjectResolver;
070import org.ametys.plugins.repository.AmetysRepositoryException;
071import org.ametys.runtime.config.Config;
072import org.ametys.runtime.i18n.I18nizableText;
073import org.ametys.runtime.servlet.RuntimeConfig;
074
075/**
076 * Content consistency engine: generate consistency information for all contents.
077 * Sends a report e-mail if there are inconsistencies.
078 */
079public class ContentConsistencyEngine implements Runnable
080{
081    
082    /** The logger. */
083    protected static final Logger _LOGGER = LoggerFactory.getLogger(ContentConsistencyEngine.class);
084    
085    /** The report e-mail will be sent to users who possess this right on the application context. */
086    protected static final String _MAIL_RIGHT = "CMS_Rights_ReceiveConsistencyReport";
087    
088    private static boolean _RUNNING;
089    
090    /** The avalon context. */
091    protected Context _context;
092    
093    /** The service manager. */
094    protected ServiceManager _manager;
095    
096    /** The server base URL. */
097    protected String _baseUrl;
098    
099    /** The report directory. */
100    protected File _reportDirectory;
101    
102    /** Is the engine initialized ? */
103    protected boolean _initialized;
104    
105    /** The cocoon environment context. */
106    protected org.apache.cocoon.environment.Context _environmentContext;
107    
108    /** The ametys object resolver. */
109    protected AmetysObjectResolver _ametysResolver;
110    
111    /** The avalon source resolver. */
112    protected SourceResolver _sourceResolver;
113    
114    /** A SAX parser. */
115    protected SAXParser _saxParser;
116    
117    /** The rights manager. */
118    protected RightManager _rightManager;
119    
120    /** The users manager. */
121    protected UserManager _userManager;
122    
123    /** The i18n utils. */
124    protected I18nUtils _i18nUtils;
125    
126    /** The content of "from" field in emails. */
127    protected String _mailFrom;
128    
129    /**
130     * Initialize the alert engine.
131     * @param manager the avalon service manager.
132     * @param context the avalon context.
133     * @throws ContextException if an error occurs retrieving the environment context.
134     * @throws ServiceException if an error occurs retrieving a component.
135     */
136    public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException
137    {
138        _manager = manager;
139        _context = context;
140        _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
141        
142        // Lookup the needed components.
143        _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
144        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
145        _saxParser = (SAXParser) manager.lookup(SAXParser.ROLE);
146        
147        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
148        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
149        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
150        
151        _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValueAsString("cms.url"), "index.html"), "/");
152        _mailFrom = Config.getInstance().getValueAsString("smtp.mail.from");
153        _reportDirectory = new File(RuntimeConfig.getInstance().getAmetysHome(), "consistency");
154        
155        _initialized = true;
156    }
157    
158    /**
159     * Configure the engine (called by the scheduler).
160     * @param configuration the component configuration.
161     * @throws ConfigurationException if an error occurred
162     */
163    public void configure(Configuration configuration) throws ConfigurationException
164    {
165        // Ignore
166    }
167    
168    /**
169     * Check the initialization and throw an exception if not initialized.
170     */
171    protected void _checkInitialization()
172    {
173        if (!_initialized)
174        {
175            String message = "Le composant de synchronisation doit être initialisé avant d'être lancé.";
176            _LOGGER.error(message);
177            throw new IllegalStateException(message);
178        }
179    }
180    
181    /**
182     * Test if the engine is running at the time.
183     * @return true if the engine is running, false otherwise.
184     */
185    static boolean isRunning()
186    {
187        return _RUNNING;
188    }
189    
190    private static void setRunning(boolean running)
191    {
192        _RUNNING = running;
193    }
194    
195    @Override
196    public void run()
197    {
198        Map<String, Object> environmentInformation = null;
199        
200        try
201        {
202            _LOGGER.info("Preparing to generate the content consistency report...");
203            
204            _checkInitialization();
205            
206            // Create the environment.
207            environmentInformation = BackgroundEngineHelper.createAndEnterEngineEnvironment(_manager, _environmentContext, new SLF4JLoggerAdapter(_LOGGER));
208            BackgroundEnvironment environment = (BackgroundEnvironment) environmentInformation.get("environment");
209
210            // Authorize workflow actions and "check-auth" CMS action, from this background environment 
211            Request request = (Request) environment.getObjectModel().get(ObjectModelHelper.REQUEST_OBJECT);
212            request.setAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INTERNAL_ALLOWED, true);
213            
214            _generateReports();
215        }
216        catch (Exception e)
217        {
218            _LOGGER.error("An error occurred generating the content consistency report.", e);
219        }
220        finally
221        {
222            // Leave the environment.
223            if (environmentInformation != null)
224            {
225                BackgroundEngineHelper.leaveEngineEnvironment(environmentInformation);
226            }
227            // Dispose of the resources.
228            _dispose();
229            _LOGGER.info("Content consistency report generated.");
230        }
231    }
232    
233    /**
234     * Dispose of the resources and looked-up components.
235     */
236    protected void _dispose()
237    {
238        // Release the components.
239        if (_manager != null)
240        {
241            _manager.release(_ametysResolver);
242            _manager.release(_rightManager);
243            _manager.release(_userManager);
244        }
245        
246        _ametysResolver = null;
247        _sourceResolver = null;
248        _rightManager = null;
249        _userManager = null;
250        
251        _environmentContext = null;
252        _context = null;
253        _manager = null;
254        
255        _initialized = false;
256    }
257    
258    /**
259     * Send all the alerts. Can be overridden to add alerts.
260     * @throws AmetysRepositoryException if an error occurs.
261     * @throws IOException if an error occurred 
262     */
263    protected void _generateReports() throws AmetysRepositoryException, IOException
264    {
265        if (isRunning())
266        {
267            _LOGGER.error("Cannot start a global consistency check, as the engine is running at the time.");
268        }
269        else
270        {
271            try
272            {
273                setRunning(true);
274                
275                Request request = ContextHelper.getRequest(_context);
276                
277                // Set the population contexts to be able to get allowed users
278                List<String> populationContexts = new ArrayList<>();
279                populationContexts.add("/application");
280
281                request.setAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR, populationContexts);
282                
283                // Generate the report.
284                _generateReport();
285            }
286            finally
287            {
288                setRunning(false);
289            }
290        }
291    }
292    
293    /**
294     * Generate the full consistency report.
295     * @throws IOException if an i/o error occurs.
296     */
297    protected void _generateReport() throws IOException
298    {
299        SitemapSource source = null;
300        File reportTmpFile = null;
301        
302        try
303        {
304            // Create the directory if it does not exist.
305            FileUtils.forceMkdir(_reportDirectory);
306            
307            // Resolve the report pipeline.
308            String url = "cocoon://_plugins/cms/consistency/inconsistent-contents-report.xml";
309            source = (SitemapSource) _sourceResolver.resolveURI(url);
310            
311            // Save the report into a temporary file.
312            reportTmpFile = new File(_reportDirectory, "report.tmp.xml");
313            OutputStream reportTmpOs = new FileOutputStream(reportTmpFile);
314            
315            SourceUtil.copy(source.getInputStream(), reportTmpOs);
316            
317            // If all went well until now, copy the temporary file to the real report file.
318            File reportFile = new File(_reportDirectory, "report.xml");
319            FileUtils.copyFile(reportTmpFile, reportFile);
320            
321            try (FileInputStream reportIs = new FileInputStream(reportFile))
322            {
323                // Parse the report to know if there were contents with inconsistencies.
324                ContentExistsHandler handler = new ContentExistsHandler();
325                _saxParser.parse(new InputSource(reportIs), handler);
326                
327                // If inconsistent contents exist, send an e-mail.
328                if (handler.hasFailures())
329                {
330                    _sendErrorEmail();
331                }
332            }
333        }
334        catch (SAXException e)
335        {
336            _LOGGER.error("The consistency report could not be parsed.", e);
337        }
338        finally
339        {
340            // Delete the temporary file.
341            if (reportTmpFile != null)
342            {
343                reportTmpFile.delete();
344            }
345
346            if (source != null)
347            {
348                _sourceResolver.release(source);
349            }
350        }
351    }
352    
353    /**
354     * Send a reminder e-mail to all the users who have the right to edit.
355     * @throws IOException if an error occurs building or sending the mail.
356     */
357    protected void _sendErrorEmail() throws IOException
358    {
359        Set<UserIdentity> users = _rightManager.getAllowedUsers(_MAIL_RIGHT, "/cms").resolveAllowedUsers(Config.getInstance().getValueAsBoolean("runtime.mail.massive.sending"));
360        
361        Map<String, String> params = _getEmailParams();
362        
363        I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_SUBJECT");
364        
365        String subject = _i18nUtils.translate(i18nSubject);
366        String body = _getMailPart(params);
367        
368        if (StringUtils.isNotEmpty(body))
369        {
370            _sendMails(subject, body, users, _mailFrom);
371        }
372    }
373    
374    /**
375     * Get a mail part.
376     * @param parameters the pipeline parameters.
377     * @return the mail part.
378     * @throws IOException if an error occurred
379     */
380    protected String _getMailPart(Map<String, String> parameters) throws IOException
381    {
382        Source source = null;
383        InputStream is = null;
384        try
385        {
386            String uri = _getMailUri(parameters);
387            source = _sourceResolver.resolveURI(uri, null, parameters);
388            is = source.getInputStream();
389            
390            ByteArrayOutputStream bos = new ByteArrayOutputStream();
391            SourceUtil.copy(is, bos);
392            
393            return bos.toString("UTF-8");
394        }
395        finally
396        {
397            if (is != null)
398            {
399                is.close();
400            }
401            
402            if (source != null)
403            {
404                _sourceResolver.release(source);
405            }
406        }
407    }
408    
409    /**
410     * Get the pipeline uri for mail body
411     * @param parameters the mail paramters
412     * @return a pipeline uri 
413     */
414    protected String _getMailUri (Map<String, String> parameters)
415    {
416        return "cocoon://_plugins/cms/consistency/inconsistent-contents-mail.html";
417    }
418    
419    /**
420     * Get the report e-mail parameters.
421     * @return the e-mail parameters.
422     */
423    protected Map<String, String> _getEmailParams()
424    {
425        Map<String, String> params = new HashMap<>();
426        
427        StringBuilder url = new StringBuilder(_baseUrl);
428        url.append("/index.html?uitool=uitool-global-consistency");
429        
430        params.put("url", url.toString());
431        
432        return params;
433    }
434    
435    /**
436     * Send the alert emails.
437     * @param subject the e-mail subject.
438     * @param body the e-mail body.
439     * @param users users to send the mail to.
440     * @param from the address sending the e-mail.
441     */
442    protected void _sendMails(String subject, String body, Set<UserIdentity> users, String from)
443    {
444        for (UserIdentity userIdentity : users)
445        {
446            User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
447            
448            if (user != null && StringUtils.isNotBlank(user.getEmail()))
449            {
450                String mail = user.getEmail();
451                
452                try
453                {
454                    SendMailHelper.sendMail(subject, null, body, mail, from);
455                }
456                catch (MessagingException e)
457                {
458                    if (_LOGGER.isWarnEnabled())
459                    {
460                        _LOGGER.warn("Could not send an alert e-mail to " + mail, e);
461                    }
462                }
463            }
464        }
465    }
466    
467    /**
468     * Handler which tests if exists a "/contents/content" tag.
469     */
470    protected class ContentExistsHandler extends DefaultHandler
471    {
472        
473        /** In content tag? */
474        protected boolean _inContentsTag;
475        
476        /** Has content tag? */
477        protected boolean _hasContent;
478        
479        /** True if the report has content with failures. */
480        protected boolean _hasFailures;
481        
482        /**
483         * Create a handler.
484         */
485        public ContentExistsHandler()
486        {
487            super();
488        }
489        
490        @Override
491        public void startDocument() throws SAXException
492        {
493            super.startDocument();
494            _inContentsTag = false;
495            _hasContent = false;
496            _hasFailures = false;
497        }
498        
499        @Override
500        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
501        {
502            super.startElement(uri, localName, qName, attributes);
503            if ("contents".equals(localName))
504            {
505                _inContentsTag = true;
506            }
507            else if (_inContentsTag && "content".equals(localName))
508            {
509                _hasContent = true;
510                
511                String notFoundCount = attributes.getValue("not-found-count");
512                String unauthorizedCount = attributes.getValue("unauthorized-count");
513                String serverErrorCount = attributes.getValue("server-error-count");
514                if (NumberUtils.toInt(notFoundCount, -1) > 0 || NumberUtils.toInt(unauthorizedCount, -1) > 0 || NumberUtils.toInt(serverErrorCount, -1) > 0)
515                {
516                    _hasFailures = true;
517                }
518            }
519        }
520        
521        @Override
522        public void endElement(String uri, String localName, String qName) throws SAXException
523        {
524            if ("contents".equals(localName))
525            {
526                _inContentsTag = false;
527            }
528            super.endElement(uri, localName, qName);
529        }
530        
531        /**
532         * Has content.
533         * @return true if the XML file has a content, false otherwise.
534         */
535        public boolean hasContent()
536        {
537            return _hasContent;
538        }
539        
540        /**
541         * Has failures.
542         * @return true if the XML file has at least a content with failures, false otherwise.
543         */
544        public boolean hasFailures()
545        {
546            return _hasFailures;
547        }
548        
549    }
550    
551}