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>&lt;user&gt;</code> elements under the root. User profile properties are attributes of the element. For example:</p>
066 * <blockquote><code>
067 * &lt;users&gt;<br/>
068 * &nbsp;&nbsp;&lt;user loginName="janne" fullName="Janne Jalkanen"<br/>
069 * &nbsp;&nbsp;&nbsp;&nbsp;wikiName="JanneJalkanen" email="janne@ecyrd.com"<br/>
070 * &nbsp;&nbsp;&nbsp;&nbsp;password="{SHA}457b08e825da547c3b77fbc1ff906a1d00a7daee"/&gt;<br/>
071 * &lt;/users&gt;
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 &lt;user&gt; 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}