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