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.ByteArrayInputStream;
019import java.io.ByteArrayOutputStream;
020import java.io.File;
021import java.io.IOException;
022import java.io.InputStream;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029
030import org.apache.avalon.framework.activity.Initializable;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.cocoon.ProcessingException;
034import org.apache.cocoon.components.ContextHelper;
035import org.apache.cocoon.environment.Request;
036import org.apache.commons.io.IOUtils;
037import org.apache.commons.lang3.StringUtils;
038import org.apache.excalibur.source.Source;
039import org.apache.excalibur.source.SourceResolver;
040import org.quartz.JobExecutionContext;
041
042import org.ametys.cms.content.consistency.ContentConsistencyManager.ConsistenciesReport;
043import org.ametys.core.right.RightManager;
044import org.ametys.core.user.User;
045import org.ametys.core.user.UserIdentity;
046import org.ametys.core.user.UserManager;
047import org.ametys.core.util.I18nUtils;
048import org.ametys.core.util.mail.SendMailHelper;
049import org.ametys.core.util.mail.SendMailHelper.NamedStream;
050import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable;
051import org.ametys.plugins.repository.AmetysObjectResolver;
052import org.ametys.runtime.config.Config;
053import org.ametys.runtime.i18n.I18nizableText;
054import org.ametys.runtime.servlet.RuntimeConfig;
055
056import jakarta.mail.MessagingException;
057
058/**
059 * Content consistency schedulable: generate consistency information for all contents.
060 * Sends a report e-mail if there are inconsistencies.
061 */
062public class CheckContentConsistencySchedulable extends AbstractStaticSchedulable implements Initializable
063{
064    /** The report e-mail will be sent to users who possess this right on the application context. */
065    protected static final String __MAIL_RIGHT = "CMS_Rights_ReceiveConsistencyReport";
066    
067    /** The server base URL. */
068    protected String _baseUrl;
069    
070    /** The report directory. */
071    protected File _reportDirectory;
072    
073    /** The ametys object resolver. */
074    protected AmetysObjectResolver _ametysResolver;
075    
076    /** The avalon source resolver. */
077    protected SourceResolver _sourceResolver;
078    
079    /** The rights manager. */
080    protected RightManager _rightManager;
081    
082    /** The i18n utils. */
083    protected I18nUtils _i18nUtils;
084    
085    /** The content of "from" field in emails. */
086    protected String _mailFrom;
087    
088    /** The content consistency manager */
089    protected ContentConsistencyManager _contentConsistencyManager;
090
091    private ContentConstitencySearchModel _contentConsistencySearchModel;
092    
093    @Override
094    public void service(ServiceManager manager) throws ServiceException
095    {
096        super.service(manager);
097        
098        _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
099        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
100        
101        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
102        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
103        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
104        
105        _contentConsistencySearchModel = (ContentConstitencySearchModel) manager.lookup(ContentConstitencySearchModel.ROLE);
106        _contentConsistencyManager = (ContentConsistencyManager) manager.lookup(ContentConsistencyManager.ROLE);
107    }
108    
109    public void initialize() throws Exception
110    {
111        _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/");
112        _mailFrom = Config.getInstance().getValue("smtp.mail.from");
113        _reportDirectory = new File(RuntimeConfig.getInstance().getAmetysHome(), "consistency");
114    }
115    
116    @Override
117    public void execute(JobExecutionContext context) throws Exception
118    {
119        ConsistenciesReport report = _contentConsistencyManager.checkAllContents();
120        
121        Set<UserIdentity> users = getUsersToNotify();
122        if (!report.isEmpty() && !users.isEmpty())
123        {
124            _sendEmail(report, users);
125        }
126    }
127
128    /**
129     * Send a reminder e-mail to all the users who have the right to edit.
130     * @param report the consistency report
131     * @param users the users to notify
132     * @throws IOException if an error occurs building or sending the mail.
133     * @throws ProcessingException if an error occurs 
134     */
135    protected void _sendEmail(ConsistenciesReport report, Set<UserIdentity> users) throws IOException, ProcessingException
136    {
137        I18nizableText i18nSubject = _getMailSubject(report);
138        String subject = _i18nUtils.translate(i18nSubject);
139        
140        I18nizableText i18nBody = _getMailBody(report);
141        String body = _i18nUtils.translate(i18nBody);
142        
143        if (StringUtils.isEmpty(body))
144        {
145            return;
146        }
147        try (InputStream attachment = _getReport(report.results()))
148        {
149            _sendMails(subject, body, users, _mailFrom, attachment);
150        }
151    }
152
153    /**
154     * Compute the list of user to notify
155     * @return a list of user identity
156     */
157    protected Set<UserIdentity> getUsersToNotify()
158    {
159        return _rightManager.getAllowedUsers(__MAIL_RIGHT, "/cms").resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"));
160    }
161    
162    private InputStream _getReport(List<String> resultsId) throws ProcessingException
163    {
164        Request request = ContextHelper.getRequest(_context);
165        Source attachement = null;
166        try
167        {
168            
169            List<Map<String, Object>> results = new ArrayList<>();
170            for (String resultId : resultsId)
171            {
172                ContentConsistencyResult result = _ametysResolver.resolveById(resultId);
173                List<Map<String, Object>> columns = _contentConsistencySearchModel.getColumns();
174                results.add(_contentConsistencyManager.resultToJSON(result, columns));
175            }
176            request.setAttribute(ContentConsistencyResultGenerator.RESULTS_REQUEST_ATTRIBUTE_NAME, results);
177            
178            attachement = _sourceResolver.resolveURI("cocoon://_plugins/cms/consistency/report.csv");
179            InputStream attachmentCopy = null;
180            // Get the attachment and store it in a reusable inputStream.
181            // If the attachment size exceed 1M we won't attach it
182            try (InputStream is = attachement.getInputStream())
183            {
184                // Override the byteArray to avoid duplication of buffer during copy
185                ByteArrayOutputStream tmp = new ByteArrayOutputStream()
186                {
187                    @Override
188                    public synchronized byte[] toByteArray()
189                    {
190                        return buf;
191                    }
192                };
193                IOUtils.copyLarge(is, tmp, 0, 1_000_000);
194                if (is.read() == -1)
195                {
196                    // We could read all the stream so the size is acceptable and we keep the attachment
197                    attachmentCopy = new ByteArrayInputStream(tmp.toByteArray());
198                }
199                
200            }
201            catch (IOException e)
202            {
203                getLogger().error("Failed to retrieve consistency report", e);
204            }
205            return attachmentCopy;
206        }
207        catch (IOException e)
208        {
209            getLogger().error("Failed to retrieve report", e);
210            return null;
211        }
212        finally
213        {
214            _sourceResolver.release(attachement);
215            request.removeAttribute(ContentConsistencyResultGenerator.RESULTS_REQUEST_ATTRIBUTE_NAME);
216        }
217    }
218
219    /**
220     * Retrieves the mail's subject
221     * @param report the consistency report
222     * @return the mail's subject
223     */
224    protected I18nizableText _getMailSubject(ConsistenciesReport report)
225    {
226        return new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_SUBJECT");
227    }
228    
229    /**
230     * Retrieves the mail's body
231     * @param report the consistency report
232     * @return the mail's body
233     */
234    protected I18nizableText _getMailBody(ConsistenciesReport report)
235    {
236        Map<String, String> params = _getEmailParams(report.results(), report.unchecked());
237
238        String failure = params.get("nbFailure");
239        String unchecked = params.get("uncheckedList");
240        
241        String key;
242        if (StringUtils.equals(failure, "0"))
243        {
244            key = "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_BODY_NO_FAILURE";
245        }
246        else if (StringUtils.equals(failure, "1"))
247        {
248            key = "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_BODY_FAILURE";
249        }
250        else
251        {
252            key = "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_BODY_FAILURES";
253        }
254        
255        if (!StringUtils.isEmpty(unchecked))
256        {
257            key += "_UNCHECKED";
258        }
259        
260        return new I18nizableText("plugin.cms", key, List.of(params.get("url"), failure, unchecked));
261    }
262    
263    /**
264     * Get the report e-mail parameters.
265     * @param unchecked the list of unchecked content id
266     * @param errorResults the list of error results
267     * @return the e-mail parameters.
268     */
269    protected Map<String, String> _getEmailParams(List<String> errorResults, List<String> unchecked)
270    {
271        Map<String, String> params = new HashMap<>();
272        
273        StringBuilder url = new StringBuilder(_baseUrl);
274        url.append("/index.html?uitool=uitool-global-consistency");
275        
276        params.put("url", url.toString());
277        params.put("nbFailure", Integer.toString(errorResults.size()));
278        params.put("uncheckedList", StringUtils.join(unchecked, ", "));
279        
280        return params;
281    }
282    
283    /**
284     * Send the alert e-mails.
285     * @param subject the e-mail subject.
286     * @param body the e-mail body.
287     * @param users users to send the mail to.
288     * @param from the address sending the e-mail.
289     * @param attachment the e-mail attachment
290     */
291    protected void _sendMails(String subject, String body, Set<UserIdentity> users, String from, InputStream attachment)
292    {
293        for (UserIdentity userIdentity : users)
294        {
295            User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
296            
297            if (user != null && StringUtils.isNotBlank(user.getEmail()))
298            {
299                String mail = user.getEmail();
300                
301                Collection<NamedStream> attachments = null;
302                if (attachment != null)
303                {
304                    try
305                    {
306                        attachment.reset();
307                    }
308                    catch (IOException e)
309                    {
310                        // in case reset is not implemented.
311                    }
312                    attachments = List.of(new NamedStream(attachment, "report.csv", "text/csv"));
313                }
314                
315                try
316                {
317                    SendMailHelper.newMail()
318                                  .withSubject(subject)
319                                  .withTextBody(body)
320                                  .withSender(from)
321                                  .withRecipient(mail)
322                                  .withAttachmentsAsStream(attachments)
323                                  .sendMail();
324                }
325                catch (MessagingException | IOException e)
326                {
327                    if (getLogger().isWarnEnabled())
328                    {
329                        getLogger().warn("Could not send an alert e-mail to " + mail, e);
330                    }
331                }
332            }
333        }
334    }
335}