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