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.HashMap;
026import java.util.Map;
027import java.util.Set;
028
029import org.apache.avalon.framework.activity.Initializable;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.components.source.impl.SitemapSource;
033import org.apache.commons.io.FileUtils;
034import org.apache.commons.lang.StringUtils;
035import org.apache.commons.lang.math.NumberUtils;
036import org.apache.excalibur.source.Source;
037import org.apache.excalibur.source.SourceResolver;
038import org.apache.excalibur.source.SourceUtil;
039import org.apache.excalibur.xml.sax.SAXParser;
040import org.quartz.JobExecutionContext;
041import org.xml.sax.Attributes;
042import org.xml.sax.InputSource;
043import org.xml.sax.SAXException;
044import org.xml.sax.helpers.DefaultHandler;
045
046import org.ametys.core.right.RightManager;
047import org.ametys.core.user.User;
048import org.ametys.core.user.UserIdentity;
049import org.ametys.core.user.UserManager;
050import org.ametys.core.util.I18nUtils;
051import org.ametys.core.util.mail.SendMailHelper;
052import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable;
053import org.ametys.plugins.repository.AmetysObjectResolver;
054import org.ametys.runtime.config.Config;
055import org.ametys.runtime.i18n.I18nizableText;
056import org.ametys.runtime.servlet.RuntimeConfig;
057
058import jakarta.mail.MessagingException;
059
060/**
061 * Content consistency schedulable: generate consistency information for all contents.
062 * Sends a report e-mail if there are inconsistencies.
063 */
064public class CheckContentConsistencySchedulable extends AbstractStaticSchedulable implements Initializable
065{
066    /** The report e-mail will be sent to users who possess this right on the application context. */
067    protected static final String _MAIL_RIGHT = "CMS_Rights_ReceiveConsistencyReport";
068    
069    /** The server base URL. */
070    protected String _baseUrl;
071    
072    /** The report directory. */
073    protected File _reportDirectory;
074    
075    /** The ametys object resolver. */
076    protected AmetysObjectResolver _ametysResolver;
077    
078    /** The avalon source resolver. */
079    protected SourceResolver _sourceResolver;
080    
081    /** The rights manager. */
082    protected RightManager _rightManager;
083    
084    /** The i18n utils. */
085    protected I18nUtils _i18nUtils;
086    
087    /** The content of "from" field in emails. */
088    protected String _mailFrom;
089    
090    @Override
091    public void service(ServiceManager manager) throws ServiceException
092    {
093        super.service(manager);
094        
095        _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
096        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
097        
098        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
099        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
100        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
101    }
102    
103    public void initialize() throws Exception
104    {
105        _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/");
106        _mailFrom = Config.getInstance().getValue("smtp.mail.from");
107        _reportDirectory = new File(RuntimeConfig.getInstance().getAmetysHome(), "consistency");
108    }
109    
110    @Override
111    public void execute(JobExecutionContext context) throws Exception
112    {
113        _generateReport();
114    }
115    
116    /**
117     * Generate the full consistency report.
118     * @throws IOException if an i/o error occurs.
119     */
120    protected void _generateReport() throws IOException
121    {
122        SitemapSource source = null;
123        File reportTmpFile = null;
124        
125        try
126        {
127            // Create the directory if it does not exist.
128            File reportDirectory = _getReportDirectory();
129            FileUtils.forceMkdir(reportDirectory);
130            
131            // Resolve the report pipeline.
132            String url = _getReportURL();
133            source = (SitemapSource) _sourceResolver.resolveURI(url);
134            
135            // Save the report into a temporary file.
136            reportTmpFile = new File(reportDirectory, "report.tmp.xml");
137            OutputStream reportTmpOs = new FileOutputStream(reportTmpFile);
138            
139            SourceUtil.copy(source.getInputStream(), reportTmpOs);
140            
141            // If all went well until now, copy the temporary file to the real report file.
142            File reportFile = new File(reportDirectory, "report.xml");
143            FileUtils.copyFile(reportTmpFile, reportFile);
144            
145            SAXParser saxParser = null;
146            try (FileInputStream reportIs = new FileInputStream(reportFile))
147            {
148                // Parse the report to know if there were contents with inconsistencies.
149                ContentExistsHandler handler = new ContentExistsHandler();
150                saxParser = (SAXParser) _smanager.lookup(SAXParser.ROLE);
151                saxParser.parse(new InputSource(reportIs), handler);
152                
153                // If inconsistent contents exist, send an e-mail.
154                if (handler.hasFailures())
155                {
156                    _sendErrorEmail();
157                }
158            }
159            finally
160            {
161                _smanager.release(saxParser);
162            }
163        }
164        catch (ServiceException e)
165        {
166            getLogger().error("Unable to get a SAX parser.", e);
167        }
168        catch (SAXException e)
169        {
170            getLogger().error("The consistency report could not be parsed.", e);
171        }
172        finally
173        {
174            // Delete the temporary file.
175            if (reportTmpFile != null)
176            {
177                reportTmpFile.delete();
178            }
179
180            if (source != null)
181            {
182                _sourceResolver.release(source);
183            }
184        }
185    }
186    
187    /**
188     * Retrieves the directory where to generate the report
189     * @return the directory where to generate the report
190     */
191    protected File _getReportDirectory()
192    {
193        return _reportDirectory;
194    }
195    
196    /**
197     * Retrieves the URL of the source to resolve to generate the report
198     * @return the URL of the source to resolve to generate the report
199     */
200    protected String _getReportURL()
201    {
202        return "cocoon://_plugins/cms/consistency/inconsistent-contents-report.xml";
203    }
204    
205    /**
206     * Send a reminder e-mail to all the users who have the right to edit.
207     * @throws IOException if an error occurs building or sending the mail.
208     */
209    protected void _sendErrorEmail() throws IOException
210    {
211        Set<UserIdentity> users = _rightManager.getAllowedUsers(_MAIL_RIGHT, "/cms").resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"));
212        
213        Map<String, String> params = _getEmailParams();
214        
215        I18nizableText i18nSubject = _getMailSubject(params);
216        String subject = _i18nUtils.translate(i18nSubject);
217
218        String body = _getMailPart(params);
219        
220        if (StringUtils.isNotEmpty(body))
221        {
222            _sendMails(subject, body, users, _mailFrom);
223        }
224    }
225    
226    /**
227     * Retrieves the mail's subject
228     * @param parameters the mail parameters.
229     * @return the mail's subject
230     */
231    protected I18nizableText _getMailSubject(Map<String, String> parameters)
232    {
233        return new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_SUBJECT");
234    }
235    
236    /**
237     * Get a mail part.
238     * @param parameters the mail parameters.
239     * @return the mail part.
240     * @throws IOException if an error occurred
241     */
242    protected String _getMailPart(Map<String, String> parameters) throws IOException
243    {
244        Source source = null;
245        InputStream is = null;
246        try
247        {
248            String uri = _getMailUri(parameters);
249            source = _sourceResolver.resolveURI(uri, null, parameters);
250            is = source.getInputStream();
251            
252            ByteArrayOutputStream bos = new ByteArrayOutputStream();
253            SourceUtil.copy(is, bos);
254            
255            return bos.toString("UTF-8");
256        }
257        finally
258        {
259            if (is != null)
260            {
261                is.close();
262            }
263            
264            if (source != null)
265            {
266                _sourceResolver.release(source);
267            }
268        }
269    }
270    
271    /**
272     * Get the pipeline uri for mail body
273     * @param parameters the mail paramters
274     * @return a pipeline uri 
275     */
276    protected String _getMailUri (Map<String, String> parameters)
277    {
278        return "cocoon://_plugins/cms/consistency/inconsistent-contents-mail.html";
279    }
280    
281    /**
282     * Get the report e-mail parameters.
283     * @return the e-mail parameters.
284     */
285    protected Map<String, String> _getEmailParams()
286    {
287        Map<String, String> params = new HashMap<>();
288        
289        StringBuilder url = new StringBuilder(_baseUrl);
290        url.append("/index.html?uitool=uitool-global-consistency");
291        
292        params.put("url", url.toString());
293        
294        return params;
295    }
296    
297    /**
298     * Send the alert emails.
299     * @param subject the e-mail subject.
300     * @param body the e-mail body.
301     * @param users users to send the mail to.
302     * @param from the address sending the e-mail.
303     */
304    protected void _sendMails(String subject, String body, Set<UserIdentity> users, String from)
305    {
306        for (UserIdentity userIdentity : users)
307        {
308            User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
309            
310            if (user != null && StringUtils.isNotBlank(user.getEmail()))
311            {
312                String mail = user.getEmail();
313                
314                try
315                {
316                    SendMailHelper.newMail()
317                                  .withSubject(subject)
318                                  .withTextBody(body)
319                                  .withSender(from)
320                                  .withRecipient(mail)
321                                  .sendMail();
322                }
323                catch (MessagingException | IOException e)
324                {
325                    if (getLogger().isWarnEnabled())
326                    {
327                        getLogger().warn("Could not send an alert e-mail to " + mail, e);
328                    }
329                }
330            }
331        }
332    }
333    
334    /**
335     * Handler which tests if exists a "/contents/content" tag.
336     */
337    protected class ContentExistsHandler extends DefaultHandler
338    {
339        
340        /** In content tag? */
341        protected boolean _inContentsTag;
342        
343        /** Has content tag? */
344        protected boolean _hasContent;
345        
346        /** True if the report has content with failures. */
347        protected boolean _hasFailures;
348        
349        /**
350         * Create a handler.
351         */
352        public ContentExistsHandler()
353        {
354            super();
355        }
356        
357        @Override
358        public void startDocument() throws SAXException
359        {
360            super.startDocument();
361            _inContentsTag = false;
362            _hasContent = false;
363            _hasFailures = false;
364        }
365        
366        @Override
367        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
368        {
369            super.startElement(uri, localName, qName, attributes);
370            if ("contents".equals(localName))
371            {
372                _inContentsTag = true;
373            }
374            else if (_inContentsTag && "content".equals(localName))
375            {
376                _hasContent = true;
377                
378                String notFoundCount = attributes.getValue("not-found-count");
379                String unauthorizedCount = attributes.getValue("unauthorized-count");
380                String serverErrorCount = attributes.getValue("server-error-count");
381                if (NumberUtils.toInt(notFoundCount, -1) > 0 || NumberUtils.toInt(unauthorizedCount, -1) > 0 || NumberUtils.toInt(serverErrorCount, -1) > 0)
382                {
383                    _hasFailures = true;
384                }
385            }
386        }
387        
388        @Override
389        public void endElement(String uri, String localName, String qName) throws SAXException
390        {
391            if ("contents".equals(localName))
392            {
393                _inContentsTag = false;
394            }
395            super.endElement(uri, localName, qName);
396        }
397        
398        /**
399         * Has content.
400         * @return true if the XML file has a content, false otherwise.
401         */
402        public boolean hasContent()
403        {
404            return _hasContent;
405        }
406        
407        /**
408         * Has failures.
409         * @return true if the XML file has at least a content with failures, false otherwise.
410         */
411        public boolean hasFailures()
412        {
413            return _hasFailures;
414        }
415    }
416    
417}