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