001/* 002 Licensed to the Apache Software Foundation (ASF) under one 003 or more contributor license agreements. See the NOTICE file 004 distributed with this work for additional information 005 regarding copyright ownership. The ASF licenses this file 006 to you under the Apache License, Version 2.0 (the 007 "License"); you may not use this file except in compliance 008 with the License. You may obtain a copy of the License at 009 010 http://www.apache.org/licenses/LICENSE-2.0 011 012 Unless required by applicable law or agreed to in writing, 013 software distributed under the License is distributed on an 014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 KIND, either express or implied. See the License for the 016 specific language governing permissions and limitations 017 under the License. 018 */ 019package org.apache.wiki.auth.user; 020 021import org.apache.commons.lang3.StringUtils; 022import org.apache.wiki.api.core.Engine; 023import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 024import org.apache.wiki.auth.NoSuchPrincipalException; 025import org.apache.wiki.auth.WikiPrincipal; 026import org.apache.wiki.auth.WikiSecurityException; 027import org.apache.wiki.util.Serializer; 028import org.apache.wiki.util.TextUtil; 029import org.w3c.dom.Document; 030import org.w3c.dom.Element; 031import org.w3c.dom.Node; 032import org.w3c.dom.NodeList; 033import org.w3c.dom.Text; 034import org.xml.sax.SAXException; 035 036import javax.xml.parsers.DocumentBuilderFactory; 037import javax.xml.parsers.ParserConfigurationException; 038import java.io.BufferedWriter; 039import java.io.File; 040import java.io.FileInputStream; 041import java.io.FileNotFoundException; 042import java.io.IOException; 043import java.io.OutputStreamWriter; 044import java.io.Serializable; 045import java.nio.charset.StandardCharsets; 046import java.nio.file.Files; 047import java.security.Principal; 048import java.text.DateFormat; 049import java.text.ParseException; 050import java.text.SimpleDateFormat; 051import java.util.Arrays; 052import java.util.Date; 053import java.util.Map; 054import java.util.Properties; 055import java.util.SortedSet; 056import java.util.TreeSet; 057import java.util.stream.Collectors; 058import java.util.stream.IntStream; 059import javax.xml.XMLConstants; 060import org.apache.commons.codec.digest.DigestUtils; 061import org.apache.commons.io.FileUtils; 062 063/** 064 * <p>Manages {@link DefaultUserProfile} objects using XML files for persistence. Passwords are hashed using SHA1. User entries are simple 065 * <code><user></code> elements under the root. User profile properties are attributes of the element. For example:</p> 066 * <blockquote><code> 067 * <users><br/> 068 * <user loginName="janne" fullName="Janne Jalkanen"<br/> 069 * wikiName="JanneJalkanen" email="janne@ecyrd.com"<br/> 070 * password="{SHA}457b08e825da547c3b77fbc1ff906a1d00a7daee"/><br/> 071 * </users> 072 * </code></blockquote> 073 * <p>In this example, the un-hashed password is <code>myP@5sw0rd</code>. Passwords are hashed without salt.</p> 074 * @since 2.3 075 */ 076 077// FIXME: If the DB is shared across multiple systems, it's possible to lose accounts 078// if two people add new accounts right after each other from different wikis. 079public class XMLUserDatabase extends AbstractUserDatabase { 080 081 /** The jspwiki.properties property specifying the file system location of the user database. */ 082 public static final String PROP_USERDATABASE = "jspwiki.xmlUserDatabaseFile"; 083 private static final String DEFAULT_USERDATABASE = "userdatabase.xml"; 084 private static final String ATTRIBUTES_TAG = "attributes"; 085 private static final String OLD_HASHES_TAG = "oldhashes"; 086 private static final String CREATED = "created"; 087 private static final String EMAIL = "email"; 088 private static final String FULL_NAME = "fullName"; 089 private static final String LOGIN_NAME = "loginName"; 090 private static final String LAST_MODIFIED = "lastModified"; 091 private static final String LOCK_EXPIRY = "lockExpiry"; 092 private static final String PASSWORD = "password"; 093 private static final String UID = "uid"; 094 private static final String USER_TAG = "user"; 095 private static final String WIKI_NAME = "wikiName"; 096 private static final String DATE_FORMAT = "yyyy.MM.dd 'at' HH:mm:ss:SSS z"; 097 private Document c_dom; 098 private File c_file; 099 private int m_passwordReusedCount = -1; 100 101 /** {@inheritDoc} */ 102 @Override 103 public synchronized void deleteByLoginName( final String loginName ) throws WikiSecurityException { 104 if( c_dom == null ) { 105 throw new WikiSecurityException( "FATAL: database does not exist" ); 106 } 107 108 final NodeList users = c_dom.getDocumentElement().getElementsByTagName( USER_TAG ); 109 for( int i = 0; i < users.getLength(); i++ ) { 110 final Element user = ( Element )users.item( i ); 111 if( user.getAttribute( LOGIN_NAME ).equals( loginName ) ) { 112 c_dom.getDocumentElement().removeChild( user ); 113 114 // Commit to disk 115 saveDOM(); 116 return; 117 } 118 } 119 throw new NoSuchPrincipalException( "Not in database: " + loginName ); 120 } 121 122 /** {@inheritDoc} */ 123 @Override 124 public UserProfile findByEmail( final String index ) throws NoSuchPrincipalException { 125 return findBy( EMAIL, index ); 126 } 127 128 /** {@inheritDoc} */ 129 @Override 130 public UserProfile findByFullName( final String index ) throws NoSuchPrincipalException { 131 return findBy( FULL_NAME, index ); 132 } 133 134 /** {@inheritDoc} */ 135 @Override 136 public UserProfile findByLoginName( final String index ) throws NoSuchPrincipalException { 137 return findBy( LOGIN_NAME, index ); 138 } 139 140 /** {@inheritDoc} */ 141 @Override 142 public UserProfile findByUid( final String uid ) throws NoSuchPrincipalException { 143 return findBy( UID, uid ); 144 } 145 146 /** {@inheritDoc} */ 147 @Override 148 public UserProfile findByWikiName( final String index ) throws NoSuchPrincipalException { 149 return findBy( WIKI_NAME, index ); 150 } 151 152 public UserProfile findBy( final String attr, final String value ) throws NoSuchPrincipalException { 153 final UserProfile profile = findByAttribute( attr, value ); 154 if ( profile != null ) { 155 return profile; 156 } 157 throw new NoSuchPrincipalException( "Not in database: " + value ); 158 } 159 160 /** {@inheritDoc} */ 161 @Override 162 public Principal[] getWikiNames() throws WikiSecurityException { 163 if ( c_dom == null ) { 164 throw new IllegalStateException( "FATAL: database does not exist" ); 165 } 166 final SortedSet< WikiPrincipal > principals = new TreeSet<>(); 167 final NodeList users = c_dom.getElementsByTagName( USER_TAG ); 168 for( int i = 0; i < users.getLength(); i++ ) { 169 final Element user = ( Element )users.item( i ); 170 final String wikiName = user.getAttribute( WIKI_NAME ); 171 if( StringUtils.isEmpty( wikiName ) ) { 172 LOG.warn( "Detected null or empty wiki name for {} in XMLUserDataBase. Check your user database.", user.getAttribute( LOGIN_NAME ) ); 173 } else { 174 final WikiPrincipal principal = new WikiPrincipal( wikiName, WikiPrincipal.WIKI_NAME ); 175 principals.add( principal ); 176 } 177 } 178 return principals.toArray( new Principal[0] ); 179 } 180 181 /** {@inheritDoc} */ 182 @Override 183 public void initialize( final Engine engine, final Properties props ) throws NoRequiredPropertyException { 184 m_engine = engine; 185 final File defaultFile; 186 if( engine.getRootPath() == null ) { 187 LOG.warn( "Cannot identify JSPWiki root path" ); 188 defaultFile = new File( "WEB-INF/" + DEFAULT_USERDATABASE ).getAbsoluteFile(); 189 } else { 190 defaultFile = new File( engine.getRootPath() + "/WEB-INF/" + DEFAULT_USERDATABASE ); 191 } 192 193 // Get database file location 194 final String file = TextUtil.getStringProperty( props, PROP_USERDATABASE, defaultFile.getAbsolutePath() ); 195 if( file == null ) { 196 LOG.warn( "XML user database property " + PROP_USERDATABASE + " not found; trying " + defaultFile ); 197 c_file = defaultFile; 198 } else { 199 c_file = new File( file ); 200 } 201 202 LOG.info( "XML user database at " + c_file.getAbsolutePath() ); 203 m_passwordReusedCount = TextUtil.getIntegerProperty( props, "jspwiki.credentials.reuseCount", -1); 204 File checkFile = new File(c_file.getParent(), c_file.getName() + ".check"); 205 if (checkFile.exists()) { 206 207 byte[] computedHash = null; 208 byte[] storedHash = null; 209 try (FileInputStream fis = new FileInputStream(c_file)) { 210 computedHash = DigestUtils.sha256(fis); 211 storedHash = FileUtils.readFileToByteArray(checkFile); 212 } catch (Exception ex) { 213 throw new RuntimeException("Failed to compute integrity check. ", ex); 214 } 215 if (Arrays.equals(computedHash, storedHash)) { 216 LOG.info("XML user database hash check passed. no modifications detected."); 217 } else { 218 throw new RuntimeException("XML user database has been modified outside of JSP Wiki. Refusing start up. An administrator will need to restore the file from backup"); 219 } 220 221 } else { 222 LOG.info("XML user database check file does not exist. This is normal if JSPWIki was just installed."); 223 } 224 225 buildDOM(); 226 sanitizeDOM(); 227 } 228 229 private void buildDOM() { 230 // Read DOM 231 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 232 factory.setValidating( false ); 233 factory.setExpandEntityReferences( false ); 234 factory.setIgnoringComments( true ); 235 factory.setNamespaceAware( false ); 236 factory.setAttribute( XMLConstants.ACCESS_EXTERNAL_DTD, "" ); 237 factory.setAttribute( XMLConstants.ACCESS_EXTERNAL_SCHEMA, "" ); 238 try { 239 c_dom = factory.newDocumentBuilder().parse( c_file ); 240 LOG.debug( "Database successfully initialized" ); 241 c_lastModified = c_file.lastModified(); 242 c_lastCheck = System.currentTimeMillis(); 243 } catch( final ParserConfigurationException e ) { 244 LOG.error( "Configuration error: {}", e.getMessage() ); 245 } catch( final SAXException e ) { 246 LOG.error( "SAX error: {}", e.getMessage() ); 247 } catch( final FileNotFoundException e ) { 248 LOG.info( "User database not found; creating from scratch..." ); 249 } catch( final IOException e ) { 250 LOG.error( "IO error: {}", e.getMessage() ); 251 } catch( final Exception e ) { 252 LOG.error( "Error initializing XML database from: {} {}", c_file.getAbsolutePath(), e.getMessage() ); 253 } 254 if ( c_dom == null ) { 255 try { 256 // Create the DOM from scratch 257 c_dom = factory.newDocumentBuilder().newDocument(); 258 c_dom.appendChild( c_dom.createElement( "users" ) ); 259 } catch( final ParserConfigurationException e ) { 260 LOG.fatal( "Could not create in-memory DOM" ); 261 } 262 } 263 } 264 265 private void saveDOM() throws WikiSecurityException { 266 if( c_dom == null ) { 267 throw new IllegalStateException( "FATAL: database does not exist" ); 268 } 269 270 final File newFile = new File( c_file.getAbsolutePath() + ".new" ); 271 try( final BufferedWriter io = new BufferedWriter( new OutputStreamWriter( Files.newOutputStream( newFile.toPath() ), StandardCharsets.UTF_8 ) ) ) { 272 273 // Write the file header and document root 274 io.write( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" ); 275 io.write( "<users>\n" ); 276 277 // Write each profile as a <user> node 278 final Element root = c_dom.getDocumentElement(); 279 final NodeList nodes = root.getElementsByTagName( USER_TAG ); 280 for( int i = 0; i < nodes.getLength(); i++ ) { 281 final Element user = ( Element )nodes.item( i ); 282 io.write( " <" + USER_TAG + " " ); 283 io.write( UID ); 284 io.write( "=\"" + user.getAttribute( UID ) + "\" " ); 285 io.write( LOGIN_NAME ); 286 io.write( "=\"" + user.getAttribute( LOGIN_NAME ) + "\" " ); 287 io.write( WIKI_NAME ); 288 io.write( "=\"" + user.getAttribute( WIKI_NAME ) + "\" " ); 289 io.write( FULL_NAME ); 290 io.write( "=\"" + user.getAttribute( FULL_NAME ) + "\" " ); 291 io.write( EMAIL ); 292 io.write( "=\"" + user.getAttribute( EMAIL ) + "\" " ); 293 io.write( PASSWORD ); 294 io.write( "=\"" + user.getAttribute( PASSWORD ) + "\" " ); 295 io.write( CREATED ); 296 io.write( "=\"" + user.getAttribute( CREATED ) + "\" " ); 297 io.write( LAST_MODIFIED ); 298 io.write( "=\"" + user.getAttribute( LAST_MODIFIED ) + "\" " ); 299 io.write( LOCK_EXPIRY ); 300 io.write( "=\"" + user.getAttribute( LOCK_EXPIRY ) + "\" " ); 301 io.write( OLD_HASHES_TAG ); 302 io.write( "=\"" + user.getAttribute( OLD_HASHES_TAG ) + "\" " ); 303 io.write( ">" ); 304 final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG ); 305 for( int j = 0; j < attributes.getLength(); j++ ) { 306 final Element attribute = ( Element )attributes.item( j ); 307 final String value = extractText( attribute ); 308 io.write( "\n <" + ATTRIBUTES_TAG + ">" ); 309 io.write( value ); 310 io.write( "</" + ATTRIBUTES_TAG + ">" ); 311 } 312 io.write( "\n </" + USER_TAG + ">\n" ); 313 } 314 io.write( "</users>" ); 315 } catch( final IOException e ) { 316 throw new WikiSecurityException( e.getLocalizedMessage(), e ); 317 } 318 319 // Copy new file over old version 320 final File backup = new File( c_file.getAbsolutePath() + ".old" ); 321 if( backup.exists() ) { 322 if( !backup.delete() ) { 323 LOG.error( "Could not delete old user database backup: " + backup ); 324 } 325 } 326 if( !c_file.renameTo( backup ) ) { 327 LOG.error( "Could not create user database backup: " + backup ); 328 } 329 if( !newFile.renameTo( c_file ) ) { 330 LOG.error( "Could not save database: " + backup + " restoring backup." ); 331 if( !backup.renameTo( c_file ) ) { 332 LOG.error( "Restore failed. Check the file permissions." ); 333 } 334 LOG.error( "Could not save database: " + c_file + ". Check the file permissions" ); 335 } 336 337 try (FileInputStream fis = new FileInputStream(c_file)) { 338 byte[] hash = DigestUtils.sha256(fis); 339 File checkFile = new File(c_file.getParent(), c_file.getName() + ".check"); 340 FileUtils.writeByteArrayToFile(checkFile, hash); 341 } catch (Exception ex) { 342 LOG.warn("Failed to recompute and/or save the check file", ex); 343 } 344 } 345 346 private long c_lastCheck; 347 private long c_lastModified; 348 349 private void checkForRefresh() { 350 final long time = System.currentTimeMillis(); 351 if( time - c_lastCheck > 60 * 1000L ) { 352 final long lastModified = c_file.lastModified(); 353 354 if( lastModified > c_lastModified ) { 355 buildDOM(); 356 } 357 } 358 } 359 360 /** 361 * {@inheritDoc} 362 * 363 * @see org.apache.wiki.auth.user.UserDatabase#rename(String, String) 364 */ 365 @Override 366 public synchronized void rename( final String loginName, final String newName) throws DuplicateUserException, WikiSecurityException { 367 if( c_dom == null ) { 368 LOG.fatal( "Could not rename profile '" + loginName + "'; database does not exist" ); 369 throw new IllegalStateException( "FATAL: database does not exist" ); 370 } 371 checkForRefresh(); 372 373 // Get the existing user; if not found, throws NoSuchPrincipalException 374 final UserProfile profile = findByLoginName( loginName ); 375 376 // Get user with the proposed name; if found, it's a collision 377 try { 378 final UserProfile otherProfile = findByLoginName( newName ); 379 if( otherProfile != null ) { 380 throw new DuplicateUserException( "security.error.cannot.rename", newName ); 381 } 382 } catch( final NoSuchPrincipalException e ) { 383 LOG.debug(e.getMessage(), e); 384 // Good! That means it's safe to save using the new name 385 } 386 387 // Find the user with the old login id attribute, and change it 388 final NodeList users = c_dom.getElementsByTagName( USER_TAG ); 389 for( int i = 0; i < users.getLength(); i++ ) { 390 final Element user = ( Element )users.item( i ); 391 if( user.getAttribute( LOGIN_NAME ).equals( loginName ) ) { 392 final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT ); 393 final Date modDate = new Date( System.currentTimeMillis() ); 394 setAttribute( user, LOGIN_NAME, newName ); 395 setAttribute( user, LAST_MODIFIED, c_format.format( modDate ) ); 396 profile.setLoginName( newName ); 397 profile.setLastModified( modDate ); 398 break; 399 } 400 } 401 402 // Commit to disk 403 saveDOM(); 404 } 405 406 /** {@inheritDoc} */ 407 @Override 408 public synchronized void save( final UserProfile profile ) throws WikiSecurityException { 409 if ( c_dom == null ) { 410 LOG.fatal( "Could not save profile " + profile + " database does not exist" ); 411 throw new IllegalStateException( "FATAL: database does not exist" ); 412 } 413 414 checkForRefresh(); 415 416 final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT ); 417 final String index = profile.getLoginName(); 418 final NodeList users = c_dom.getElementsByTagName( USER_TAG ); 419 Element user = IntStream.range(0, users.getLength()).mapToObj(i -> (Element) users.item(i)).filter(currentUser -> currentUser.getAttribute(LOGIN_NAME).equals(index)).findFirst().orElse(null); 420 421 boolean isNew = false; 422 423 final Date modDate = new Date( System.currentTimeMillis() ); 424 if( user == null ) { 425 // Create new user node 426 profile.setCreated( modDate ); 427 LOG.info( "Creating new user " + index ); 428 user = c_dom.createElement( USER_TAG ); 429 c_dom.getDocumentElement().appendChild( user ); 430 setAttribute( user, CREATED, c_format.format( profile.getCreated() ) ); 431 isNew = true; 432 } else { 433 // To update existing user node, delete old attributes first... 434 final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG ); 435 for( int i = 0; i < attributes.getLength(); i++ ) { 436 user.removeChild( attributes.item( i ) ); 437 } 438 } 439 440 setAttribute( user, UID, profile.getUid() ); 441 setAttribute( user, LAST_MODIFIED, c_format.format( modDate ) ); 442 setAttribute( user, LOGIN_NAME, profile.getLoginName() ); 443 setAttribute( user, FULL_NAME, profile.getFullname() ); 444 setAttribute( user, WIKI_NAME, profile.getWikiName() ); 445 setAttribute( user, EMAIL, profile.getEmail() ); 446 final Date lockExpiry = profile.getLockExpiry(); 447 setAttribute( user, LOCK_EXPIRY, lockExpiry == null ? "" : c_format.format( lockExpiry ) ); 448 449 // Hash and save the new password if it's different from old one 450 final String newPassword = profile.getPassword(); 451 if( newPassword != null && !newPassword.equals( "" ) ) { 452 final String oldPassword = user.getAttribute( PASSWORD ); 453 if( !oldPassword.equals( newPassword ) ) { 454 String newhash = getHash( newPassword ); 455 setAttribute( user, PASSWORD, newhash ); 456 457 profile.getPreviousHashedCredentials().add(newhash); 458 while (!profile.getPreviousHashedCredentials().isEmpty() && 459 profile.getPreviousHashedCredentials().size() > m_passwordReusedCount) { 460 profile.getPreviousHashedCredentials().remove(0); 461 } 462 463 } 464 } 465 466 // Save the attributes as Base64 string 467 if(!profile.getAttributes().isEmpty()) { 468 try { 469 final String encodedAttributes = Serializer.serializeToBase64( profile.getAttributes() ); 470 final Element attributes = c_dom.createElement( ATTRIBUTES_TAG ); 471 user.appendChild( attributes ); 472 final Text value = c_dom.createTextNode( encodedAttributes ); 473 attributes.appendChild( value ); 474 } catch( final IOException e ) { 475 throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e ); 476 } 477 } 478 if (!profile.getPreviousHashedCredentials().isEmpty()) { 479 setAttribute( user, OLD_HASHES_TAG, StringUtils.join(profile.getPreviousHashedCredentials(), "|")); 480 } 481 482 // Set the profile timestamps 483 if( isNew ) { 484 profile.setCreated( modDate ); 485 } 486 profile.setLastModified( modDate ); 487 488 // Commit to disk 489 saveDOM(); 490 } 491 492 /** 493 * Private method that returns the first {@link UserProfile}matching a <user> element's supplied attribute. This method will also 494 * set the UID if it has not yet been set. 495 * 496 * @param matchAttribute matching attribute 497 * @param index value to match 498 * @return the profile, or <code>null</code> if not found 499 */ 500 private UserProfile findByAttribute( final String matchAttribute, String index ) { 501 if ( c_dom == null ) { 502 throw new IllegalStateException( "FATAL: database does not exist" ); 503 } 504 505 checkForRefresh(); 506 final NodeList users = c_dom.getElementsByTagName( USER_TAG ); 507 if( users == null ) { 508 return null; 509 } 510 511 // check if we have to do a case-insensitive compare 512 final boolean caseSensitiveCompare = !matchAttribute.equals( EMAIL ); 513 514 for( int i = 0; i < users.getLength(); i++ ) { 515 final Element user = (Element) users.item( i ); 516 String userAttribute = user.getAttribute( matchAttribute ); 517 if( !caseSensitiveCompare ) { 518 userAttribute = StringUtils.lowerCase(userAttribute); 519 index = StringUtils.lowerCase(index); 520 } 521 if( userAttribute.equals( index ) ) { 522 final UserProfile profile = newProfile(); 523 524 // Parse basic attributes 525 profile.setUid( user.getAttribute( UID ) ); 526 if( profile.getUid() == null || profile.getUid().isEmpty() ) { 527 profile.setUid( generateUid( this ) ); 528 } 529 profile.setLoginName( user.getAttribute( LOGIN_NAME ) ); 530 profile.setFullname( user.getAttribute( FULL_NAME ) ); 531 profile.setPassword( user.getAttribute( PASSWORD ) ); 532 profile.setEmail( user.getAttribute( EMAIL ) ); 533 534 // Get created/modified timestamps 535 final String created = user.getAttribute( CREATED ); 536 final String modified = user.getAttribute( LAST_MODIFIED ); 537 profile.setCreated( parseDate( profile, created ) ); 538 profile.setLastModified( parseDate( profile, modified ) ); 539 540 // Is the profile locked? 541 final String lockExpiry = user.getAttribute( LOCK_EXPIRY ); 542 if( StringUtils.isEmpty( lockExpiry ) || lockExpiry.isEmpty() ) { 543 profile.setLockExpiry( null ); 544 } else { 545 profile.setLockExpiry( new Date( Long.parseLong( lockExpiry ) ) ); 546 } 547 final String oldHahes = user.getAttribute(OLD_HASHES_TAG); 548 if (oldHahes != null && oldHahes.length() > 0) { 549 String[] parts = oldHahes.split("\\|"); 550 for (String s : parts) { 551 profile.getPreviousHashedCredentials().add(s); 552 } 553 } 554 555 // Extract all the user's attributes (should only be one attributes tag, but you never know!) 556 final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG ); 557 for( int j = 0; j < attributes.getLength(); j++ ) { 558 final Element attribute = ( Element )attributes.item( j ); 559 final String serializedMap = extractText( attribute ); 560 try { 561 final Map< String, ? extends Serializable > map = Serializer.deserializeFromBase64( serializedMap ); 562 profile.getAttributes().putAll( map ); 563 } catch( final IOException e ) { 564 LOG.error( "Could not parse user profile attributes!", e ); 565 } 566 } 567 568 return profile; 569 } 570 } 571 return null; 572 } 573 574 /** 575 * Extracts all the text nodes that are immediate children of an Element. 576 * 577 * @param element the base element 578 * @return the text nodes that are immediate children of the base element, concatenated together 579 */ 580 private String extractText( final Element element ) { 581 String text = ""; 582 if( element.getChildNodes().getLength() > 0 ) { 583 final NodeList children = element.getChildNodes(); 584 text = IntStream.range(0, children.getLength()).mapToObj(children::item).filter(child -> child.getNodeType() == Node.TEXT_NODE).map(child -> ((Text) child).getData()).collect(Collectors.joining()); 585 } 586 return text; 587 } 588 589 /** 590 * Tries to parse a date using the default format - then, for backwards compatibility reasons, tries the platform default. 591 * 592 * @param profile profile associated to the date. 593 * @param date date to be parsed. 594 * @return A parsed date, or null, if both parse attempts fail. 595 */ 596 private Date parseDate( final UserProfile profile, final String date ) { 597 try { 598 final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT ); 599 return c_format.parse( date ); 600 } catch( final ParseException e ) { 601 try { 602 return DateFormat.getDateTimeInstance().parse( date ); 603 } catch( final ParseException e2 ) { 604 LOG.warn( "Could not parse 'created' or 'lastModified' attribute for profile '" + profile.getLoginName() + "'." + 605 " It may have been tampered with.", e2 ); 606 } 607 } 608 return null; 609 } 610 611 /** 612 * After loading the DOM, this method sanity-checks the dates in the DOM and makes sure they are formatted properly. This is sort-of 613 * hacky, but it should work. 614 */ 615 private void sanitizeDOM() { 616 if( c_dom == null ) { 617 throw new IllegalStateException( "FATAL: database does not exist" ); 618 } 619 620 final NodeList users = c_dom.getElementsByTagName( USER_TAG ); 621 for( int i = 0; i < users.getLength(); i++ ) { 622 final Element user = ( Element )users.item( i ); 623 624 // Sanitize UID (and generate a new one if one does not exist) 625 String uid = user.getAttribute( UID ).trim(); 626 if( StringUtils.isEmpty( uid ) || "-1".equals( uid ) ) { 627 uid = String.valueOf( generateUid( this ) ); 628 user.setAttribute( UID, uid ); 629 } 630 631 // Sanitize dates 632 final String loginName = user.getAttribute( LOGIN_NAME ); 633 String created = user.getAttribute( CREATED ); 634 String modified = user.getAttribute( LAST_MODIFIED ); 635 final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT ); 636 try { 637 created = c_format.format( c_format.parse( created ) ); 638 modified = c_format.format( c_format.parse( modified ) ); 639 user.setAttribute( CREATED, created ); 640 user.setAttribute( LAST_MODIFIED, modified ); 641 } catch( final ParseException e ) { 642 try { 643 created = c_format.format( DateFormat.getDateTimeInstance().parse( created ) ); 644 modified = c_format.format( DateFormat.getDateTimeInstance().parse( modified ) ); 645 user.setAttribute( CREATED, created ); 646 user.setAttribute( LAST_MODIFIED, modified ); 647 } catch( final ParseException e2 ) { 648 LOG.warn( "Could not parse 'created' or 'lastModified' attribute for profile '" + loginName + "'." 649 + " It may have been tampered with." ); 650 } 651 } 652 } 653 } 654 655 /** 656 * Private method that sets an attribute value for a supplied DOM element. 657 * 658 * @param element the element whose attribute is to be set 659 * @param attribute the name of the attribute to set 660 * @param value the desired attribute value 661 */ 662 private void setAttribute( final Element element, final String attribute, final String value ) { 663 if( value != null ) { 664 element.setAttribute( attribute, value ); 665 } 666 } 667 668}