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