001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.jsp.userdata; 029 030import org.opencms.ade.detailpage.CmsDetailPageInfo; 031import org.opencms.configuration.CmsParameterConfiguration; 032import org.opencms.configuration.I_CmsXmlConfiguration; 033import org.opencms.file.CmsObject; 034import org.opencms.file.CmsResource; 035import org.opencms.file.CmsUser; 036import org.opencms.file.CmsUserSearchParameters; 037import org.opencms.file.CmsUserSearchParameters.SortKey; 038import org.opencms.jsp.util.CmsJspStandardContextBean; 039import org.opencms.mail.CmsHtmlMail; 040import org.opencms.main.CmsException; 041import org.opencms.main.CmsLog; 042import org.opencms.main.OpenCms; 043import org.opencms.report.I_CmsReport; 044import org.opencms.security.CmsRole; 045import org.opencms.util.CmsStringUtil; 046import org.opencms.xml.content.CmsXmlContent; 047import org.opencms.xml.content.CmsXmlContentFactory; 048 049import java.util.ArrayList; 050import java.util.Arrays; 051import java.util.Collections; 052import java.util.List; 053import java.util.Optional; 054import java.util.ServiceLoader; 055import java.util.stream.Collectors; 056 057import javax.mail.internet.AddressException; 058import javax.mail.internet.InternetAddress; 059 060import org.apache.commons.digester3.Digester; 061import org.apache.commons.digester3.Rule; 062import org.apache.commons.logging.Log; 063import org.apache.commons.mail.EmailException; 064 065import org.dom4j.Element; 066import org.jsoup.Jsoup; 067import org.jsoup.nodes.Document; 068import org.xml.sax.Attributes; 069 070/** 071 * Manager class for user data requests.<p> 072 * 073 * Users can request their data either via username/password, in which case the user data will be collected for that user, 074 * or by email, in which case the data for all users with the given email address is collected. 075 * <p> 076 * User data is formatted as HTML by one or more configurable 'user data domain' classes, which implement the 077 * I_CmsUserDataDomain interface. 078 * 079 * <p>After the user requests their data, they are sent an email with a link (which is only valid for a certain time). When 080 * opening that link, they have to confirm the credentials they requested the data with, and if they successfully do so, 081 * are shown a page with their user data. 082 */ 083public class CmsUserDataRequestManager { 084 085 /** The name that must be used for the function detail pages for user data requests. */ 086 public static final String FUNCTION_NAME = "userdatarequest"; 087 088 /** Tag name for the user data request manager. */ 089 public static final String N_USERDATA = "userdata"; 090 091 /** Attribute to enable/disable autoloading of plugins via service loader.*/ 092 public static final String A_AUTOLOAD = "autoload"; 093 094 /** Tag name for the user data domain. */ 095 public static final String N_USERDATA_DOMAIN = "userdata-domain"; 096 097 /** Logger for this class. */ 098 private static final Log LOG = CmsLog.getLog(CmsUserDataRequestManager.class); 099 100 /** A CmsObject with admin privileges. */ 101 private CmsObject m_adminCms; 102 103 /** The configured user data domains. */ 104 private List<I_CmsUserDataDomain> m_configuredDomains = new ArrayList<>(); 105 106 /** The store used to load/save user data requests. */ 107 private CmsUserDataRequestStore m_requestStore = new CmsUserDataRequestStore(); 108 109 /** If true, additional plugins should be loaded via service loader. */ 110 private boolean m_autoload; 111 112 /** List of all domains, both the ones from the configuration and the ones loaded via service loader. */ 113 private List<I_CmsUserDataDomain> m_allDomains = new ArrayList<>(); 114 115 /** 116 * Adds digester rules for configuration. 117 * 118 * @param digester the digester 119 * @param basePath the base xpath for the configuration of user data requests 120 */ 121 public static void addDigesterRules(Digester digester, String basePath) { 122 123 digester.addObjectCreate(basePath, CmsUserDataRequestManager.class); 124 digester.addRule(basePath, new Rule() { 125 126 private boolean m_autoload; 127 128 @Override 129 public void begin(String namespace, String name, Attributes attributes) throws Exception { 130 131 m_autoload = Boolean.parseBoolean(attributes.getValue(A_AUTOLOAD)); 132 } 133 134 @Override 135 public void end(String namespace, String name) throws Exception { 136 137 CmsUserDataRequestManager manager = digester.peek(); 138 manager.setAutoload(m_autoload); 139 } 140 141 }); 142 String domainPath = basePath + "/" + N_USERDATA_DOMAIN; 143 digester.addObjectCreate(domainPath, null, I_CmsXmlConfiguration.A_CLASS); 144 digester.addSetNext(domainPath, "addUserDataDomain"); 145 // use pre-existing rules for params in system configuration to set the parameters for the user domain 146 } 147 148 /** 149 * Gets all users with a given email address (maximum of 999). 150 * 151 * @param cms the CMS context 152 * @param email the email address 153 * @return the list of users 154 * @throws CmsException if something goes wrong 155 */ 156 public static List<CmsUser> getUsersByEmail(CmsObject cms, String email) throws CmsException { 157 158 if (CmsStringUtil.isEmptyOrWhitespaceOnly(email)) { 159 // we don't want to find the users with no email address! 160 return new ArrayList<>(); 161 } 162 CmsUserSearchParameters params = new CmsUserSearchParameters(); 163 params.setPaging(9999, 1); 164 params.setFilterEmail(email); 165 params.setSorting(SortKey.email, true); 166 List<CmsUser> users = OpenCms.getOrgUnitManager().searchUsers(cms, params); 167 return users; 168 } 169 170 /** 171 * Adds a user data domain during configuration phase. 172 * 173 * @param domain the user data domain to add 174 */ 175 public void addUserDataDomain(I_CmsUserDataDomain domain) { 176 177 checkNotInitialized(); 178 m_configuredDomains.add(domain); 179 } 180 181 /** 182 * Writes the configuration back to XML. 183 * 184 * @param element the parent element 185 */ 186 public void appendToXml(Element element) { 187 188 Element root = element.addElement(N_USERDATA); 189 if (isAutoload()) { 190 root.addAttribute(A_AUTOLOAD, "true"); 191 } 192 for (I_CmsUserDataDomain domain : m_configuredDomains) { 193 String clsName = domain.getClass().getName(); 194 Element domainElem = root.addElement(N_USERDATA_DOMAIN); 195 domainElem.addAttribute(I_CmsXmlConfiguration.A_CLASS, clsName); 196 CmsParameterConfiguration config = domain.getConfiguration(); 197 config.appendToXml(domainElem); 198 } 199 } 200 201 /** 202 * Checks that the manager has not already been initialized, and throws an exception otherwise. 203 */ 204 public void checkNotInitialized() { 205 206 if (m_adminCms != null) { 207 throw new IllegalStateException("CmsUserDataRequestManager already initialized."); 208 } 209 } 210 211 /** 212 * Gets the list of all user data domains, both those from the configuration and those loaded via service loader. 213 * 214 * @return the list of all user data domains 215 */ 216 public List<I_CmsUserDataDomain> getAllDomains() { 217 218 return m_allDomains; 219 } 220 221 /** 222 * Gets the user data for an email address. 223 * 224 * <p>Only callable by root admin users. 225 * 226 * @param cms the CMS context 227 * @param mode the mode 228 * @param email the email address (may be null) 229 * @param searchStrings a list of additional search strings entered by the user 230 * @param root the root element to which the report should be added 231 * @param report the report to write to 232 * @return true if the HTML document was changed as a result of executing this method 233 * 234 * @throws CmsException if something goes wrong 235 */ 236 public boolean getInfoForEmail( 237 CmsObject cms, 238 I_CmsUserDataDomain.Mode mode, 239 String email, 240 List<String> searchStrings, 241 org.jsoup.nodes.Element root, 242 I_CmsReport report) 243 throws CmsException { 244 245 OpenCms.getRoleManager().checkRole(cms, CmsRole.ROOT_ADMIN); 246 return internalGetInfoForEmail(cms, mode, email, searchStrings, root, report); 247 } 248 249 /** 250 * Gets the user data for a specific OpenCms user. 251 * 252 * <p>Only callable by root admin users. 253 * 254 * @param cms the CMS context 255 * @param mode the mode 256 * @param user the OpenCms user 257 * @param root the root element to which the report should be added 258 * @param report the report to write to 259 * @return true if the HTML document was changed as a result of executing this method 260 * @throws CmsException if something goes wrong 261 * 262 */ 263 public boolean getInfoForUser( 264 CmsObject cms, 265 I_CmsUserDataDomain.Mode mode, 266 CmsUser user, 267 org.jsoup.nodes.Element root, 268 I_CmsReport report) 269 throws CmsException { 270 271 OpenCms.getRoleManager().checkRole(cms, CmsRole.ROOT_ADMIN); 272 return internalGetInfoForUser(cms, mode, user, root, report); 273 } 274 275 /** 276 * Gets the user data request store. 277 * 278 * @return the user data request store 279 */ 280 public CmsUserDataRequestStore getRequestStore() { 281 282 return m_requestStore; 283 } 284 285 /** 286 * Initializes the manager. 287 * 288 * @param cms a CMS context with admin privileges 289 */ 290 public void initialize(CmsObject cms) { 291 292 checkNotInitialized(); 293 m_adminCms = cms; 294 m_allDomains.addAll(m_configuredDomains); 295 if (m_autoload) { 296 m_allDomains.addAll(loadUserDataDomainsFromClasses()); 297 } 298 for (I_CmsUserDataDomain domain : m_allDomains) { 299 domain.initialize(cms); 300 } 301 m_requestStore.initialize(cms); 302 } 303 304 /** 305 * Loads the user data request configuration from the given file. 306 * 307 * <p>Returns null if no configuration is found. 308 * 309 * @param cms the CMS context 310 * @param path the site path of the configuration 311 * @return the configuration for the given path 312 */ 313 public Optional<CmsUserDataRequestConfig> loadConfig(CmsObject cms, String path) { 314 315 LOG.debug("loading user data request config for path " + path); 316 if (path == null) { 317 LOG.info("path is null"); 318 return Optional.empty(); 319 } 320 try { 321 CmsResource resource = cms.readResource(path); 322 CmsXmlContent content = CmsXmlContentFactory.unmarshal(cms, cms.readFile(resource)); 323 CmsUserDataRequestConfig result = new CmsUserDataRequestConfig( 324 cms, 325 content, 326 cms.getRequestContext().getLocale()); 327 return Optional.of(result); 328 } catch (CmsException e) { 329 LOG.error(e.getLocalizedMessage(), e); 330 return Optional.empty(); 331 } 332 } 333 334 /** 335 * Starts a user data request for the single user case (with user name and password).<p> 336 * 337 * @param cms the CMS context 338 * @param config the configuration 339 * @param user the user for which the data was requested 340 * 341 * @throws AddressException if parsing the email address fails 342 * @throws EmailException if sending the email fails 343 */ 344 public void startUserDataRequest(CmsObject cms, CmsUserDataRequestConfig config, CmsUser user) 345 throws AddressException, EmailException { 346 347 Document doc = Jsoup.parseBodyFragment(""); 348 org.jsoup.nodes.Element root = doc.body().appendElement("div"); 349 CmsUserDataRequestInfo info = new CmsUserDataRequestInfo(); 350 info.setUser(user.getName()); 351 info.setType(CmsUserDataRequestType.singleUser); 352 info.setEmail(user.getEmail()); 353 info.setExpiration(System.currentTimeMillis() + config.getRequestLifetime()); 354 for (I_CmsUserDataDomain userDomain : getAllDomains()) { 355 if (userDomain.matchesUser(cms, CmsUserDataRequestType.singleUser, user)) { 356 userDomain.appendInfoHtml( 357 cms, 358 CmsUserDataRequestType.singleUser, 359 Collections.singletonList(user), 360 root); 361 } 362 } 363 info.setInfoHtml(root.toString()); 364 m_requestStore.save(info); 365 sendMail(cms, config, user.getEmail(), info.getId()); 366 } 367 368 /** 369 * Starts a user data request for the email case. 370 * 371 * @param cms the CMS context 372 * @param config the user data request configuration 373 * @param email the email address 374 * @throws CmsUserDataRequestException if something goes wrong 375 * @throws EmailException if sending the email fails 376 * @throws AddressException if parsing the email address fails 377 */ 378 public void startUserDataRequest(CmsObject cms, CmsUserDataRequestConfig config, String email) 379 throws CmsUserDataRequestException, EmailException, AddressException { 380 381 if (CmsStringUtil.isEmptyOrWhitespaceOnly(email)) { 382 throw new IllegalArgumentException("Can not use empty email address for user data request by email."); 383 } 384 try { 385 List<CmsUser> users = getUsersByEmail(m_adminCms, email); 386 Document doc = Jsoup.parseBodyFragment(""); 387 org.jsoup.nodes.Element root = doc.body().appendElement("div"); 388 389 boolean foundDomain = false; 390 for (I_CmsUserDataDomain userDomain : getAllDomains()) { 391 List<CmsUser> usersForDomain = new ArrayList<>(); 392 for (CmsUser user : users) { 393 if (userDomain.matchesUser(cms, CmsUserDataRequestType.email, user)) { 394 usersForDomain.add(user); 395 foundDomain = true; 396 } 397 } 398 if (!usersForDomain.isEmpty()) { 399 userDomain.appendInfoHtml(cms, CmsUserDataRequestType.email, usersForDomain, root); 400 } 401 } 402 if (!foundDomain) { 403 throw new CmsUserDataRequestException("no users found with the given email address."); 404 } 405 CmsUserDataRequestInfo info = new CmsUserDataRequestInfo(); 406 info.setType(CmsUserDataRequestType.email); 407 info.setEmail(email); 408 info.setInfoHtml(root.toString()); 409 info.setExpiration(System.currentTimeMillis() + config.getRequestLifetime()); 410 m_requestStore.save(info); 411 sendMail(cms, config, email, info.getId()); 412 } catch (CmsException e) { 413 throw new CmsUserDataRequestException(e); 414 } 415 416 } 417 418 /** 419 * @see java.lang.Object#toString() 420 */ 421 @Override 422 public String toString() { 423 424 String collectorsStr = getAllDomains().stream().map(domain -> domain.toString()).collect( 425 Collectors.joining(", ")); 426 return getClass().getName() + "[" + collectorsStr + "]"; 427 } 428 429 /** 430 * Sets the autoload flag, which enables automatic loading of plugins via service loader. 431 * 432 * @param autoload the value of the autoload flag 433 */ 434 protected void setAutoload(boolean autoload) { 435 436 checkNotInitialized(); 437 m_autoload = autoload; 438 } 439 440 /** 441 * Internal helper method for getting the user data for an email address. 442 * 443 * @param cms the CMS context 444 * @param mode the mode 445 * @param email the email address 446 * @param searchStrings an additional list of search strings entered by the user 447 * @param root the root element to which the report should be added 448 * @param report the report to write to 449 * @return true if the HTML document was changed as a result of executing this method 450 * @throws CmsException if something goes wrong 451 */ 452 private boolean internalGetInfoForEmail( 453 CmsObject cms, 454 I_CmsUserDataDomain.Mode mode, 455 String email, 456 List<String> searchStrings, 457 org.jsoup.nodes.Element root, 458 I_CmsReport report) 459 throws CmsException { 460 461 Document doc = root.ownerDocument(); 462 String oldHtml = doc.toString(); 463 464 List<CmsUser> users = getUsersByEmail(m_adminCms, email); 465 boolean foundDomain = false; 466 int i = 0; 467 for (I_CmsUserDataDomain userDomain : getAllDomains()) { 468 i += 1; 469 report.print( 470 Messages.get().container(Messages.RPT_USERDATADOMAIN_COUNT_2, "" + i, "" + getAllDomains().size())); 471 report.print( 472 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0), 473 I_CmsReport.FORMAT_DEFAULT); 474 if (!userDomain.isAvailableForMode(mode)) { 475 report.println( 476 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_SKIPPED_0), 477 I_CmsReport.FORMAT_DEFAULT); 478 continue; 479 } 480 List<CmsUser> usersForDomain = new ArrayList<>(); 481 for (CmsUser user : users) { 482 if (userDomain.matchesUser(cms, CmsUserDataRequestType.email, user)) { 483 usersForDomain.add(user); 484 foundDomain = true; 485 } 486 } 487 if (!usersForDomain.isEmpty()) { 488 userDomain.appendInfoHtml(cms, CmsUserDataRequestType.email, usersForDomain, root); 489 } 490 userDomain.appendlInfoForEmail(cms, email, searchStrings, root); 491 report.println( 492 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 493 I_CmsReport.FORMAT_OK); 494 } 495 String newHtml = doc.toString(); 496 boolean changed = !(newHtml.equals(oldHtml)); 497 return changed; 498 } 499 500 /** 501 * Internal helper method for getting the user data for a specific OpenCms user. 502 * 503 * @param cms the CMS context 504 * @param mode the mode 505 * @param user the OpenCms user 506 * @param root the root element to which the report should be added 507 * @param report the report 508 * @return true if the HTML document was changed as a result of executing this method 509 * 510 */ 511 private boolean internalGetInfoForUser( 512 CmsObject cms, 513 I_CmsUserDataDomain.Mode mode, 514 CmsUser user, 515 org.jsoup.nodes.Element root, 516 I_CmsReport report) { 517 518 Document doc = root.ownerDocument(); 519 String oldHtml = doc.toString(); 520 int i = 0; 521 for (I_CmsUserDataDomain userDomain : getAllDomains()) { 522 i += 1; 523 report.print( 524 Messages.get().container(Messages.RPT_USERDATADOMAIN_COUNT_2, "" + i, "" + getAllDomains().size())); 525 report.print( 526 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0), 527 I_CmsReport.FORMAT_DEFAULT); 528 if (!userDomain.isAvailableForMode(mode)) { 529 report.println( 530 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_SKIPPED_0), 531 I_CmsReport.FORMAT_DEFAULT); 532 continue; 533 } 534 List<CmsUser> usersForDomain = new ArrayList<>(); 535 if (userDomain.matchesUser(cms, CmsUserDataRequestType.email, user)) { 536 usersForDomain.add(user); 537 } 538 if (!usersForDomain.isEmpty()) { 539 userDomain.appendInfoHtml(cms, CmsUserDataRequestType.email, usersForDomain, root); 540 } 541 report.println( 542 org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0), 543 I_CmsReport.FORMAT_OK); 544 } 545 String newHtml = doc.toString(); 546 boolean changed = !(newHtml.equals(oldHtml)); 547 return changed; 548 } 549 550 /** 551 * Returns true if the autoload flag is enabled. 552 * 553 * @return true if the autoload flag is enabled 554 */ 555 private boolean isAutoload() { 556 557 return m_autoload; 558 } 559 560 /** 561 * Loads additional plugins via service loader. 562 * 563 * @return the additional plugins loaded 564 */ 565 private List<I_CmsUserDataDomain> loadUserDataDomainsFromClasses() { 566 567 List<I_CmsUserDataDomain> result = new ArrayList<>(); 568 ServiceLoader<I_CmsUserDataDomainProvider> loader = ServiceLoader.load(I_CmsUserDataDomainProvider.class); 569 for (I_CmsUserDataDomainProvider provider : loader) { 570 for (I_CmsUserDataDomain domain : provider.getUserDataDomains()) { 571 result.add(domain); 572 } 573 } 574 return result; 575 } 576 577 /** 578 * Sends the user data request mail to the given email address. 579 * 580 * @param cms the CMS context 581 * @param config the configuration 582 * @param email the email address 583 * @param id the user data request id 584 * @throws EmailException if sending the email fails 585 * @throws AddressException if parsing the address fails 586 */ 587 private void sendMail(CmsObject cms, CmsUserDataRequestConfig config, String email, String id) 588 throws EmailException, AddressException { 589 590 CmsHtmlMail mail = new CmsHtmlMail(); 591 mail.setCharset("UTF-8"); 592 593 mail.setSubject(config.getMailSubject()); 594 mail.setTo(Arrays.asList(InternetAddress.parse(email))); 595 String link = CmsJspStandardContextBean.getFunctionDetailLink( 596 cms, 597 CmsDetailPageInfo.FUNCTION_PREFIX, 598 FUNCTION_NAME, 599 true) 600 + "?" 601 + CmsJspUserDataRequestBean.PARAM_UDRID 602 + "=" 603 + id 604 + "&" 605 + CmsJspUserDataRequestBean.PARAM_ACTION 606 + "=" 607 + CmsJspUserDataRequestBean.ACTION_VIEW; 608 mail.setHtmlMsg(config.getMailText() + "<a href=\"" + link + "\">" + link + "</a>"); 609 mail.send(); 610 611 } 612}