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.web.alerts;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Calendar;
021import java.util.GregorianCalendar;
022import java.util.HashSet;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027
028import org.apache.avalon.framework.configuration.Configuration;
029import org.apache.avalon.framework.configuration.ConfigurationException;
030import org.apache.cocoon.components.ContextHelper;
031import org.apache.cocoon.environment.Request;
032import org.apache.commons.lang.StringUtils;
033import org.quartz.JobDataMap;
034import org.quartz.JobExecutionContext;
035
036import org.ametys.cms.repository.Content;
037import org.ametys.core.schedule.progression.ContainerProgressionTracker;
038import org.ametys.core.ui.mail.StandardMailBodyHelper;
039import org.ametys.core.user.UserIdentity;
040import org.ametys.core.user.population.PopulationContextHelper;
041import org.ametys.core.util.URIUtils;
042import org.ametys.plugins.core.schedule.Scheduler;
043import org.ametys.plugins.repository.AmetysObjectIterable;
044import org.ametys.plugins.repository.AmetysRepositoryException;
045import org.ametys.plugins.repository.query.expression.DateExpression;
046import org.ametys.plugins.repository.query.expression.Expression;
047import org.ametys.plugins.repository.query.expression.Expression.Operator;
048import org.ametys.runtime.config.Config;
049import org.ametys.runtime.i18n.I18nizableText;
050import org.ametys.web.repository.content.WebContent;
051import org.ametys.web.repository.page.Page;
052import org.ametys.web.repository.page.PageQueryHelper;
053import org.ametys.web.repository.page.jcr.DefaultPage;
054import org.ametys.web.repository.site.Site;
055
056/**
057 * Alerts engine: sends alerts mail.
058 * This is the web version of the engine: it sets the currently processed content's
059 * site name in the request object, and sends additional site and page information
060 * in the alerts e-mails.
061 */
062public class AlertSchedulable extends org.ametys.cms.alerts.AlertSchedulable
063{
064    /** True if the alert of page publication ending is enabled */
065    protected boolean _pagePublicationEnabled;
066    
067    /** The "page nearing end of publication" e-mail will be sent to users that have this right. */
068    protected Set<String> _pagePublicationEndRights;
069    
070    /** The "page nearing end of publication" e-mail subject i18n key. */
071    protected String _pagePublicationEndSubject;
072    
073    /** The "page nearing end of publication" e-mail body i18n key. */
074    protected String _pagePublicationEndBody;
075    
076    /** The server base URL. */
077    protected String _baseUrl;
078    
079    @Override
080    public void initialize() throws Exception
081    {
082        super.initialize();
083        _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/");
084    }
085    
086    @Override
087    public void configure(Configuration configuration) throws ConfigurationException
088    {
089        super.configure(configuration);
090        
091        Configuration pagePublicationEndConf = configuration.getChild("pagePublicationEnd", false);
092        
093        if (pagePublicationEndConf != null)
094        {
095            _pagePublicationEnabled = true;
096            _pagePublicationEndRights = _getRightsFromConf(pagePublicationEndConf);
097            _pagePublicationEndSubject = pagePublicationEndConf.getChild("subjectKey").getValue();
098            _pagePublicationEndBody = pagePublicationEndConf.getChild("bodyKey").getValue();
099        }
100    }
101    
102    @Override
103    public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception
104    {
105        // Send the content alerts.
106        super.execute(context, progressionTracker);
107        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
108        boolean instantMode = jobDataMap.getBooleanValue(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_INSTANT_MODE_KEY);
109        if (!instantMode && _pagePublicationEnabled)
110        {
111            // Send the page alerts.
112            _sendPagePublicationEndAlerts();
113        }
114    }
115    
116    @Override
117    protected void _setRequestAttributes (Content content)
118    {
119        Request request = ContextHelper.getRequest(_context);
120        
121        Site site = _getSite(content);
122        if (site != null)
123        {
124            String siteName = site.getName();
125            // Set the site name into the request for the other components to be able to retrieve it.
126            request.setAttribute("siteName", siteName);
127            
128            List<String> populationContexts = new ArrayList<>();
129            
130            // Set the population contexts to be able to get allowed users
131            populationContexts.add("/sites/" + siteName);
132            populationContexts.add("/sites-fo/" + siteName);
133            
134            request.setAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR, populationContexts);
135        }
136    }
137    
138    /**
139     * Set the necessary request attributes 
140     * @param page The concerned page
141     */
142    protected void _setRequestAttributes (Page page)
143    {
144        Request request = ContextHelper.getRequest(_context);
145        
146        String siteName = page.getSiteName();
147        
148        // Set the site name into the request for the other components to be able to retrieve it.
149        request.setAttribute("siteName", siteName);
150        
151        List<String> populationContexts = new ArrayList<>();
152        
153        // Set the population contexts to be able to get allowed users
154        populationContexts.add("/sites/" + siteName);
155        populationContexts.add("/sites-fo/" + siteName);
156        
157        request.setAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR, populationContexts);
158    }
159    
160    /**
161     * Send the "page nearing end of publication" alerts.
162     * @throws AmetysRepositoryException if an error occurs.
163     */
164    protected void _sendPagePublicationEndAlerts() throws AmetysRepositoryException
165    {
166        Long delay = Config.getInstance().getValue("remind.before.publication.end");
167        if (delay != null && delay > 0)
168        {
169            Calendar calendar = new GregorianCalendar();
170            _removeTimeParts(calendar);
171            calendar.add(Calendar.DAY_OF_MONTH, delay.intValue());
172            
173            // Get all the pages which publication end is in X days.
174            Expression nearPublicationEndExpr = new DateExpression(DefaultPage.METADATA_PUBLICATION_END_DATE, Operator.EQ, calendar.getTime());
175            
176            String query = PageQueryHelper.getPageXPathQuery(null, null, null, nearPublicationEndExpr, null);
177            
178            try (AmetysObjectIterable<Page> pages = _resolver.query(query))
179            {
180                if (getLogger().isInfoEnabled())
181                {
182                    getLogger().info("Pages within " + Long.toString(delay) + " days of publication end: " + pages.getSize());
183                }
184                
185                for (Page page : pages)
186                {
187                    // Send the alert e-mails.
188                    _sendPagePublicationEndEmail(page);
189                }
190            }
191        }
192    }
193    
194    /**
195     * Send the "page publication end" alert e-mail.
196     * @param page the page nearing publication end.
197     * @throws AmetysRepositoryException if an error occurs.
198     */
199    protected void _sendPagePublicationEndEmail(Page page) throws AmetysRepositoryException
200    {
201        _setRequestAttributes(page);
202        
203        Set<UserIdentity> users = new HashSet<>();
204        for (String right : _pagePublicationEndRights)
205        {
206            users.addAll(_rightManager.getAllowedUsers(right, page).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
207        }
208        
209        List<String> params = _getPagePublicationEndParams(page);
210        
211        I18nizableText i18nSubject = new I18nizableText(null, _pagePublicationEndSubject, params);
212        I18nizableText i18nBody = new I18nizableText(null, _pagePublicationEndBody, params);
213        
214        String subject = _i18nUtils.translate(i18nSubject);
215        String body = _i18nUtils.translate(i18nBody);
216        
217        try
218        {
219            body = StandardMailBodyHelper.newHTMLBody()
220                    .withTitle(i18nSubject)
221                    .withMessage(i18nBody)
222                    .withLink(_getPageUrl(page), new I18nizableText("plugin.web", "CONTENT_ALERTS_MAIL_PAGE_LINK_TITLE"))
223                    .build();
224            
225            _sendMails(subject, body, users, _mailFrom);
226        }
227        catch (IOException e)
228        {
229            getLogger().error("Fail to build HTML mail body for page publication end", e);
230        }
231    }
232    
233    @Override
234    protected String getI18nKeyBody(String bodyI18nKey, Content content)
235    {
236        String i18nKey = bodyI18nKey;
237        
238        Site site = _getSite(content);
239        if (site == null)
240        {
241            i18nKey += "_NOSITE";
242        }
243        else if (_getPage(content, site) == null)
244        {
245            // Orphan content
246            i18nKey += "_ORPHAN";
247        }
248        
249        return i18nKey;
250    }
251    
252    @Override
253    protected List<String> _getAdditionalParams(Content content)
254    {
255        List<String> params = new ArrayList<>();
256        
257        Site site = _getSite(content);
258        if (site != null)
259        {
260            params.add(site.getTitle());  // {4}
261        }
262        
263        Page page = _getPage(content, site);
264        if (page != null)
265        {
266            params.add(page.getTitle()); // {5}
267        }
268        
269        return params;
270    }
271    
272    /**
273     * Get the "page nearing end of publication" mail parameters.
274     * @param page the page.
275     * @return the mail parameters.
276     */
277    protected List<String> _getPagePublicationEndParams(Page page)
278    {
279        List<String> params = new ArrayList<>();
280        
281        String siteTitle = page.getSite().getTitle();
282        Long delay = Config.getInstance().getValue("remind.before.publication.end");
283        
284        params.add(page.getTitle());
285        params.add(_getPageUrl(page));
286        params.add(String.valueOf(delay));
287        params.add(siteTitle);
288        
289        return params;
290    }
291    
292    @Override
293    protected String _getContentUrl(Content content)
294    {
295        Site site = _getSite(content);
296        if (site != null)
297        {
298            Page page = _getPage(content, site);
299            if (page != null)
300            {
301                StringBuilder url = new StringBuilder(_baseUrl);
302                if (!_baseUrl.endsWith("/"))
303                {
304                    url.append('/');
305                }
306                url.append(site.getName()).append("/index.html?uitool=uitool-page,id:%27").append(URIUtils.encodeParameter(page.getId())).append("%27");
307                return url.toString();
308            }
309            else
310            {
311                return _contentHelper.getContentBOUrl(content, Map.of("siteName", site.getName()));
312            }
313        }
314        else
315        {
316            return super._getContentUrl(content);
317        }
318    }
319    
320    /**
321     * Get the URL of the back-office, opening on the page tool.
322     * @param page the page to link to.
323     * @return the page URL.
324     */
325    protected String _getPageUrl(Page page)
326    {
327        StringBuilder url = new StringBuilder(_baseUrl);
328        if (!_baseUrl.endsWith("/"))
329        {
330            url.append('/');
331        }
332        url.append(page.getSite().getName()).append("/index.html?uitool=uitool-page,id:%27").append(page.getId()).append("%27");
333        return url.toString();
334    }
335    
336    /**
337     * Get the site name
338     * @param content The content
339     * @return the site name
340     */
341    protected Site _getSite(Content content)
342    {
343        if (content instanceof WebContent)
344        {
345            return ((WebContent) content).getSite();
346        }
347        
348        return null;
349    }
350
351    /**
352     * Get the page referenced by this content
353     * @param content The content
354     * @param site the site
355     * @return the page or null.
356     */
357    protected Page _getPage(Content content, Site site)
358    {
359        if (content instanceof WebContent)
360        {
361            Iterator<Page> pages = ((WebContent) content).getReferencingPages().iterator();
362            if (pages.hasNext())
363            {
364                return pages.next();
365            }
366        }
367        return null;
368    }
369    
370}