    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;
    020
    021import org.apache.commons.lang3.StringUtils;
    022import org.apache.logging.log4j.LogManager;
    023import org.apache.logging.log4j.Logger;
    024import org.apache.wiki.ajax.AjaxUtil;
    025import org.apache.wiki.ajax.WikiAjaxDispatcherServlet;
    026import org.apache.wiki.ajax.WikiAjaxServlet;
    027import org.apache.wiki.api.core.Context;
    028import org.apache.wiki.api.core.Engine;
    029import org.apache.wiki.api.core.Session;
    030import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
    031import org.apache.wiki.api.exceptions.WikiException;
    032import org.apache.wiki.api.filters.PageFilter;
    033import org.apache.wiki.auth.permissions.AllPermission;
    034import org.apache.wiki.auth.permissions.WikiPermission;
    035import org.apache.wiki.auth.user.DummyUserDatabase;
    036import org.apache.wiki.auth.user.DuplicateUserException;
    037import org.apache.wiki.auth.user.UserDatabase;
    038import org.apache.wiki.auth.user.UserProfile;
    039import org.apache.wiki.event.WikiEventListener;
    040import org.apache.wiki.event.WikiEventManager;
    041import org.apache.wiki.event.WikiSecurityEvent;
    042import org.apache.wiki.filters.FilterManager;
    043import org.apache.wiki.filters.SpamFilter;
    044import org.apache.wiki.i18n.InternationalizationManager;
    045import org.apache.wiki.pages.PageManager;
    046import org.apache.wiki.preferences.Preferences;
    047import org.apache.wiki.tasks.TasksManager;
    048import org.apache.wiki.ui.InputValidator;
    049import org.apache.wiki.util.ClassUtil;
    050import org.apache.wiki.util.TextUtil;
    051import org.apache.wiki.workflow.Decision;
    052import org.apache.wiki.workflow.DecisionRequiredException;
    053import org.apache.wiki.workflow.Fact;
    054import org.apache.wiki.workflow.Step;
    055import org.apache.wiki.workflow.Workflow;
    056import org.apache.wiki.workflow.WorkflowBuilder;
    057import org.apache.wiki.workflow.WorkflowManager;
    058
    059import jakarta.servlet.ServletException;
    060import jakarta.servlet.http.HttpServletRequest;
    061import jakarta.servlet.http.HttpServletResponse;
    062import java.io.IOException;
    063import java.security.Permission;
    064import java.security.Principal;
    065import java.text.MessageFormat;
    066import java.util.List;
    067import java.util.Map;
    068import java.util.NoSuchElementException;
    069import java.util.Properties;
    070import java.util.ResourceBundle;
    071import java.util.WeakHashMap;
    072import org.apache.wiki.auth.authorize.Group;
    073import org.apache.wiki.auth.authorize.GroupManager;
    074import tools.jackson.databind.ObjectMapper;
    075import tools.jackson.databind.node.ObjectNode;
    076
    077
    078
    079/**
    080 * Default implementation for {@link UserManager}.
    081 *
    082 * @since 2.3
    083 */
    084public class DefaultUserManager implements UserManager {
    085
    086    private static final String USERDATABASE_PACKAGE = "org.apache.wiki.auth.user";
    087    public static final String SESSION_MESSAGES = "profile";
    088    private static final String PARAM_EMAIL = "email";
    089    private static final String PARAM_FULLNAME = "fullname";
    090    private static final String PARAM_PASSWORD = "password";
    091    private static final String PARAM_LOGINNAME = "loginname";
    092    private static final String UNKNOWN_CLASS = "<unknown>";
    093
    094    private Engine m_engine;
    095
    096    private static final Logger LOG = LogManager.getLogger( DefaultUserManager.class);
    097
    098    /** Associates wiki sessions with profiles */
    099    private final Map< Session, UserProfile > m_profiles = new WeakHashMap<>();
    100
    101    /** The user database loads, manages and persists user identities */
    102    private UserDatabase m_database;
    103
    104    /** {@inheritDoc} */
    105    @Override
    106    public void initialize( final Engine engine, final Properties props ) {
    107        m_engine = engine;
    108
    109        // Attach the PageManager as a listener
    110        // TODO: it would be better if we did this in PageManager directly
    111        addWikiEventListener( engine.getManager( PageManager.class ) );
    112
    113        //TODO: Replace with custom annotations. See JSPWIKI-566
    114        WikiAjaxDispatcherServlet.registerServlet( JSON_USERS, new JSONUserModule(this), new AllPermission(null));
    115    }
    116
    117    /** {@inheritDoc} */
    118    @Override
    119    public UserDatabase getUserDatabase() {
    120        if( m_database != null ) {
    121            return m_database;
    122        }
    123
    124        String dbClassName = UNKNOWN_CLASS;
    125
    126        try {
    127            dbClassName = TextUtil.getRequiredProperty( m_engine.getWikiProperties(), PROP_DATABASE );
    128
    129            LOG.info( "Attempting to load user database class {}", dbClassName );
    130            m_database = ClassUtil.buildInstance( USERDATABASE_PACKAGE, dbClassName );
    131            m_database.initialize( m_engine, m_engine.getWikiProperties() );
    132            LOG.info( "UserDatabase initialized." );
    133        } catch( final NoSuchElementException | NoRequiredPropertyException e ) {
    134            LOG.error( "You have not set the '{}'. You need to do this if you want to enable user management by JSPWiki.", PROP_DATABASE, e );
    135        } catch( final ReflectiveOperationException e ) {
    136            LOG.error( "UserDatabase {} cannot be instantiated", dbClassName, e );
    137        } catch( final WikiSecurityException e ) {
    138            LOG.error( "Exception initializing user database: {}", e.getMessage(), e );
    139        } finally {
    140            if( m_database == null ) {
    141                LOG.info( "I could not create a database object you specified (or didn't specify), so I am falling back to a default." );
    142                m_database = new DummyUserDatabase();
    143            }
    144        }
    145
    146        return m_database;
    147    }
    148
    149    /** {@inheritDoc} */
    150    @Override
    151    public UserProfile getUserProfile( final Session session ) {
    152        // Look up cached user profile
    153        UserProfile profile = m_profiles.get( session );
    154        boolean newProfile = profile == null;
    155        Principal user = null;
    156
    157        // If user is authenticated, figure out if this is an existing profile
    158        if ( session.isAuthenticated() ) {
    159            user = session.getUserPrincipal();
    160            try {
    161                profile = getUserDatabase().findByWikiName( user.getName());
    162                newProfile = false;
    163            } catch( final NoSuchPrincipalException e ) {
    164                LOG.debug(e.getMessage(), e);
    165            }
    166        }
    167
    168        if ( newProfile ) {
    169            profile = getUserDatabase().newProfile();
    170            if ( user != null ) {
    171                profile.setLoginName( user.getName() );
    172            } else {
    173                LOG.warn("new profile however the user principal is null. this shouldn't happen");
    174            }
    175            if ( !profile.isNew() ) {
    176                throw new IllegalStateException( "New profile should be marked 'new'. Check your UserProfile implementation." );
    177            }
    178        }
    179
    180        // Stash the profile for next time
    181        m_profiles.put( session, profile );
    182        return profile;
    183    }
    184
    185    /** {@inheritDoc} */
    186    @Override
    187    public void setUserProfile( final Context context, final UserProfile profile ) throws DuplicateUserException, WikiException {
    188        final Session session = context.getWikiSession();
    189        // Verify user is allowed to save profile!
    190        final Permission p = new WikiPermission( m_engine.getApplicationName(), WikiPermission.EDIT_PROFILE_ACTION );
    191        if ( !m_engine.getManager( AuthorizationManager.class ).checkPermission( session, p ) ) {
    192            throw new WikiSecurityException( "You are not allowed to save wiki profiles." );
    193        }
    194
    195        // Check if profile is new, and see if container allows creation
    196        final boolean newProfile = profile.isNew();
    197
    198        // Check if another user profile already has the fullname or loginname
    199        final UserProfile oldProfile = getUserProfile( session );
    200        final boolean nameChanged = ( oldProfile != null && oldProfile.getFullname() != null ) &&
    201                                    !( oldProfile.getFullname().equals( profile.getFullname() ) &&
    202                                    oldProfile.getLoginName().equals( profile.getLoginName() ) );
    203        UserProfile otherProfile;
    204        try {
    205            otherProfile = getUserDatabase().findByLoginName( profile.getLoginName() );
    206            if( otherProfile != null && !otherProfile.equals( oldProfile ) ) {
    207                throw new DuplicateUserException( "security.error.login.taken", profile.getLoginName() );
    208            }
    209        } catch (final NoSuchPrincipalException e) {
    210            LOG.debug(e.getMessage(), e);
    211        }
    212        try {
    213            otherProfile = getUserDatabase().findByFullName( profile.getFullname() );
    214            if( otherProfile != null && !otherProfile.equals( oldProfile ) ) {
    215                throw new DuplicateUserException( "security.error.fullname.taken", profile.getFullname() );
    216            }
    217        } catch( final NoSuchPrincipalException e ) {
    218            LOG.debug(e.getMessage(), e);
    219        }
    220
    221        // For new accounts, create approval workflow for user profile save.
    222        if( newProfile && oldProfile != null && oldProfile.isNew() ) {
    223            startUserProfileCreationWorkflow( context, profile );
    224
    225            // If the profile doesn't need approval, then just log the user in
    226
    227            try {
    228                final AuthenticationManager mgr = m_engine.getManager( AuthenticationManager.class );
    229                if( !mgr.isContainerAuthenticated() ) {
    230                    mgr.login( session, null, profile.getLoginName(), profile.getPassword() );
    231                }
    232            } catch( final WikiException e ) {
    233                throw new WikiSecurityException( e.getMessage(), e );
    234            }
    235
    236            // Alert all listeners that the profile changed...
    237            // ...this will cause credentials to be reloaded in the wiki session
    238            fireEvent( WikiSecurityEvent.PROFILE_SAVE, session, profile );
    239        } else { // For existing accounts, just save the profile
    240            // If login name changed, rename it first
    241            if( nameChanged && !oldProfile.getLoginName().equals( profile.getLoginName() ) ) {
    242                getUserDatabase().rename( oldProfile.getLoginName(), profile.getLoginName() );
    243            }
    244
    245            // Now, save the profile (userdatabase will take care of timestamps for us)
    246            getUserDatabase().save( profile );
    247
    248            if( nameChanged ) {
    249                // Fire an event if the login name or full name changed
    250                final UserProfile[] profiles = new UserProfile[] { oldProfile, profile };
    251                fireEvent( WikiSecurityEvent.PROFILE_NAME_CHANGED, session, profiles );
    252            } else {
    253                // Fire an event that says we have new a new profile (new principals)
    254                fireEvent( WikiSecurityEvent.PROFILE_SAVE, session, profile );
    255            }
    256        }
    257        m_profiles.put( session, profile );
    258    }
    259
    260    /** {@inheritDoc} */
    261    @Override
    262    public void startUserProfileCreationWorkflow( final Context context, final UserProfile profile ) throws WikiException {
    263        final WorkflowBuilder builder = WorkflowBuilder.getBuilder( m_engine );
    264        final Principal submitter = context.getWikiSession().getUserPrincipal();
    265        final Step completionTask = m_engine.getManager( TasksManager.class ).buildSaveUserProfileTask( context.getWikiSession().getLocale() );
    266
    267        // Add user profile attribute as Facts for the approver (if required)
    268        final boolean hasEmail = profile.getEmail() != null;
    269        final Fact[] facts = new Fact[ hasEmail ? 4 : 3 ];
    270        facts[ 0 ] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_PREFS_FULL_NAME, profile.getFullname() );
    271        facts[ 1 ] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_PREFS_LOGIN_NAME, profile.getLoginName() );
    272        facts[ 2 ] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_SUBMITTER, submitter.getName() );
    273        if ( hasEmail ) {
    274            facts[ 3 ] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_PREFS_EMAIL, profile.getEmail() );
    275        }
    276        final Workflow workflow = builder.buildApprovalWorkflow( submitter,
    277                                                                 WorkflowManager.WF_UP_CREATE_SAVE_APPROVER,
    278                                                                 null,
    279                                                                 WorkflowManager.WF_UP_CREATE_SAVE_DECISION_MESSAGE_KEY,
    280                                                                 facts,
    281                                                                 completionTask,
    282                                                                 null );
    283
    284        workflow.setAttribute( WorkflowManager.WF_UP_CREATE_SAVE_ATTR_SAVED_PROFILE, profile );
    285        workflow.start( context );
    286
    287        final boolean approvalRequired = workflow.getCurrentStep() instanceof Decision;
    288
    289        // If the profile requires approval, redirect user to message page
    290        if ( approvalRequired ) {
    291            throw new DecisionRequiredException( "This profile must be approved before it becomes active" );
    292        }
    293    }
    294
    295    /** {@inheritDoc} */
    296    @Override
    297    public UserProfile parseProfile( final Context context ) {
    298        // Retrieve the user's profile (may have been previously cached)
    299        final UserProfile profile = getUserProfile( context.getWikiSession() );
    300        final HttpServletRequest request = context.getHttpRequest();
    301
    302        // Extract values from request stream (cleanse whitespace as needed)
    303        String loginName = request.getParameter( PARAM_LOGINNAME );
    304        String password = request.getParameter( PARAM_PASSWORD );
    305        String fullname = request.getParameter( PARAM_FULLNAME );
    306        String email = request.getParameter( PARAM_EMAIL );
    307        loginName = StringUtils.trim( loginName );
    308        password = InputValidator.isBlank( password ) ? null : password;
    309        fullname = StringUtils.trim( fullname );
    310        email = StringUtils.trim( email );
    311
    312        // A special case if we have container authentication: if authenticated, login name is always taken from container
    313        if ( m_engine.getManager( AuthenticationManager.class ).isContainerAuthenticated() && context.getWikiSession().isAuthenticated() ) {
    314            loginName = context.getWikiSession().getLoginPrincipal().getName();
    315        }
    316
    317        // Set the profile fields!
    318        profile.setLoginName( loginName );
    319        profile.setEmail( email );
    320        profile.setFullname( fullname );
    321        profile.setPassword( password );
    322        return profile;
    323    }
    324
    325    /** {@inheritDoc} */
    326    @Override
    327    public void validateProfile( final Context context, final UserProfile profile ) {
    328        final Session session = context.getWikiSession();
    329        final InputValidator validator = new InputValidator( SESSION_MESSAGES, context );
    330        final ResourceBundle rb = Preferences.getBundle( context, InternationalizationManager.CORE_BUNDLE );
    331
    332        //  Query the SpamFilter first
    333        final FilterManager fm = m_engine.getManager( FilterManager.class );
    334        final List< PageFilter > ls = fm.getFilterList();
    335        for( final PageFilter pf : ls ) {
    336            if( pf instanceof SpamFilter ) {
    337                if( !( ( SpamFilter )pf ).isValidUserProfile( context, profile ) ) {
    338                    session.addMessage( SESSION_MESSAGES, "Invalid userprofile" );
    339                    return;
    340                }
    341                break;
    342            }
    343        }
    344
    345        // If container-managed auth and user not logged in, throw an error
    346        if ( m_engine.getManager( AuthenticationManager.class ).isContainerAuthenticated()
    347             && !context.getWikiSession().isAuthenticated() ) {
    348            session.addMessage( SESSION_MESSAGES, rb.getString("security.error.createprofilebeforelogin") );
    349        }
    350
    351        validator.validateNotNull( profile.getLoginName(), rb.getString("security.user.loginname") );
    352        validator.validateNotNull( profile.getFullname(), rb.getString("security.user.fullname") );
    353        validator.validate( profile.getEmail(), rb.getString("security.user.email"), InputValidator.EMAIL );
    354
    355        if( !m_engine.getManager( AuthenticationManager.class ).isContainerAuthenticated() ) {
    356            // passwords must match and can't be null
    357
    358            //this is the new password
    359            final String newpassword = profile.getPassword();
    360            if( newpassword == null ) {
    361                session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.blankpassword" ) );
    362            } else {
    363                final HttpServletRequest request = context.getHttpRequest();
    364                //the existing password
    365                final String existingPassword = ( request == null ) ? null : request.getParameter( "password0" );
    366                //the new password confirmation
    367                final String passwordConfirmation = ( request == null ) ? null : request.getParameter( "password2" );
    368                if (!newpassword.equals(passwordConfirmation)) {
    369                    //password confirmation does not match
    370                    session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.passwordnomatch" ) );
    371                }
    372                if( !profile.isNew() && (existingPassword==null || existingPassword.equals( newpassword ) ) ) {
    373                    //existing account and the existing password matches the new password
    374                    session.addMessage( SESSION_MESSAGES, "existing password matches the proposed new one" );
    375                }
    376                if( !profile.isNew() && !getUserDatabase().validatePassword( profile.getLoginName(), existingPassword ) ) {
    377                    //existing account and the provided password does not match what we currently have
    378                    session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.passwordnomatch" ) );
    379                }
    380                List<String> msg = PasswordComplexityVerifier.validate(passwordConfirmation, existingPassword, context);
    381                for (String s : msg) {
    382                    session.addMessage( SESSION_MESSAGES, s );
    383                }
    384                int reuseCount = Integer.parseInt(m_engine.getWikiProperties().getProperty("jspwiki.credentials.reuseCount", "-1"));
    385                if (reuseCount > 0) {
    386                    //if it's set to 0 or less, we don't store it so we can skip this check
    387                    if (!m_database.validatePasswordReuse(profile.getLoginName(), passwordConfirmation)) {
    388                        //password reuse detected
    389                        session.addMessage(SESSION_MESSAGES,
    390                                MessageFormat.format(rb.getString("security.error.passwordReuseError"), reuseCount));
    391                    }
    392                }
    393            }
    394        }
    395
    396        UserProfile otherProfile;
    397        final String fullName = profile.getFullname();
    398        final String loginName = profile.getLoginName();
    399        final String wikiName = profile.getWikiName();
    400        final String email = profile.getEmail();
    401
    402
    403        if ("true".equalsIgnoreCase(m_engine.getWikiProperties().getProperty(Engine.PROP_USE_2_X_ACL_LOGIC, "false"))) {
    404            // It's illegal to use as a full name someone else's login name
    405            try {
    406                otherProfile = getUserDatabase().findByFullName(fullName );
    407                if( otherProfile != null && !profile.equals( otherProfile ) && !fullName.equals( otherProfile.getFullname() ) ) {
    408                    final Object[] args = { fullName };
    409                    session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.illegalfullname" ), args ) );
    410                }
    411            } catch( final NoSuchPrincipalException e ) {
    412                LOG.debug(e.getMessage(), e);
    413                /* It's clean */
    414            }
    415
    416            // It's illegal to use as a login name someone else's full name
    417            try {
    418                otherProfile = getUserDatabase().findByLoginName(loginName );
    419                if( otherProfile != null && !profile.equals( otherProfile ) && !loginName.equals( otherProfile.getLoginName() ) ) {
    420                    final Object[] args = { loginName };
    421                    session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.illegalloginname" ), args ) );
    422                }
    423            } catch( final NoSuchPrincipalException e ) {
    424                LOG.debug(e.getMessage(), e);
    425                /* It's clean */
    426            }
    427        } else {
    428            //JSPWIKI-130, v3+ behavior
    429            // It is legal to use as a full name someone else's login name
    430
    431            // It's illegal to use as a login name someone else's full name
    432            try {
    433                otherProfile = getUserDatabase().findByLoginName(loginName );
    434                if( otherProfile != null && !profile.equals( otherProfile ) && !loginName.equals( otherProfile.getLoginName() ) ) {
    435                    final Object[] args = { loginName };
    436                    session.addMessage( SESSION_MESSAGES, MessageFormat.format(
    437                            rb.getString( "security.error.illegalloginname" ), args ) );
    438                }
    439            } catch( final NoSuchPrincipalException e ) {
    440                LOG.debug(e.getMessage(), e);
    441                /* It's clean */
    442            }
    443            //it's illegal to use a username, email or wiki name as a group name
    444            try {
    445                Group[] groups = m_engine.getManager(GroupManager.class).getGroupDatabase().groups();
    446                for (Group grp : groups) {
    447                    if (grp.getName().equals(loginName)) {
    448                        final Object[] args = {loginName};
    449                        session.addMessage(SESSION_MESSAGES,
    450                                MessageFormat.format(rb.getString("security.error.illegalloginname"), args));
    451                    }
    452                    if (grp.getName().equals(wikiName)) {
    453                        final Object[] args = {wikiName};
    454                        session.addMessage(SESSION_MESSAGES,
    455                                MessageFormat.format(rb.getString("security.error.illegalloginname"), args));
    456                    }
    457                    if (grp.getName().equals(email)) {
    458                        final Object[] args = {email};
    459                        session.addMessage(SESSION_MESSAGES,
    460                                MessageFormat.format(rb.getString("security.error.illegalloginname"), args));
    461                    }
    462                }
    463            } catch (WikiSecurityException ex) {
    464                session.addMessage(SESSION_MESSAGES,
    465                        "Processing failed. see log for details.");
    466                LOG.error("failed to query for groups", ex);
    467            }
    468            //wiki names must be unique as well.
    469            try {
    470                otherProfile = getUserDatabase().findByWikiName(wikiName );
    471                if( otherProfile != null && !profile.equals( otherProfile ) && !loginName.equals( otherProfile.getLoginName() ) ) {
    472                    final Object[] args = { loginName };
    473                    session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.illegalloginname" ), args ) );
    474                }
    475            } catch( final NoSuchPrincipalException e ) {
    476                LOG.debug(e.getMessage(), e);
    477                /* It's clean */
    478            }
    479        }
    480
    481
    482        // It's illegal to use multiple accounts with the same email
    483        if (email != null && email.trim().length() > 0) {
    484            try {
    485                otherProfile = getUserDatabase().findByEmail( email );
    486                if( otherProfile != null && !profile.getUid().equals( otherProfile.getUid() ) // Issue JSPWIKI-1042
    487                        && !profile.equals( otherProfile ) && StringUtils.lowerCase( email )
    488                        .equals( StringUtils.lowerCase( otherProfile.getEmail() ) ) ) {
    489                    final Object[] args = { email };
    490                    session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.email.taken" ), args ) );
    491                }
    492            } catch( final NoSuchPrincipalException e ) {
    493                LOG.debug(e.getMessage(), e);
    494                /* It's clean */
    495            }
    496        }
    497    }
    498
    499    /** {@inheritDoc} */
    500    @Override
    501    public Principal[] listWikiNames() throws WikiSecurityException {
    502        return getUserDatabase().getWikiNames();
    503    }
    504
    505    // events processing .......................................................
    506
    507    /**
    508     * Registers a WikiEventListener with this instance.
    509     * This is a convenience method.
    510     * @param listener the event listener
    511     */
    512    @Override public synchronized void addWikiEventListener( final WikiEventListener listener ) {
    513        WikiEventManager.addWikiEventListener( this, listener );
    514    }
    515
    516    /**
    517     * Un-registers a WikiEventListener with this instance.
    518     * This is a convenience method.
    519     * @param listener the event listener
    520     */
    521    @Override public synchronized void removeWikiEventListener( final WikiEventListener listener ) {
    522        WikiEventManager.removeWikiEventListener( this, listener );
    523    }
    524
    525    /**
    526     *  Implements the JSON API for usermanager.
    527     *  <p>
    528     *  Even though this gets serialized whenever container shuts down/restarts, this gets reinstalled to the session when JSPWiki starts.
    529     *  This means that it's not actually necessary to save anything.
    530     */
    531    public static final class JSONUserModule implements WikiAjaxServlet {
    532
    533        private final DefaultUserManager m_manager;
    534
    535        /**
    536         *  Create a new JSONUserModule.
    537         *  @param mgr Manager
    538         */
    539        public JSONUserModule( final DefaultUserManager mgr )
    540        {
    541            m_manager = mgr;
    542        }
    543
    544        @Override
    545        public String getServletMapping() {
    546            return JSON_USERS;
    547        }
    548
    549        @Override
    550        public void service( final HttpServletRequest req, final HttpServletResponse resp, final String actionName, final List<String> params) throws ServletException, IOException {
    551            try {
    552                if( params.isEmpty() ) {
    553                    return;
    554                }
    555                resp.setContentType("application/json");
    556                final String uid = params.get(0);
    557                LOG.debug("uid="+uid);
    558                if (StringUtils.isNotBlank(uid)) {
    559                    final UserProfile prof = getUserInfo(uid);
    560                    //clone the object
    561                    ObjectMapper om = new ObjectMapper();
    562                    ObjectNode node = om.convertValue(prof, ObjectNode.class);
    563                    node.remove("password");
    564                    node.remove("previousHashedCredentials");
    565                    resp.getWriter().write(node.toString());
    566                }
    567            } catch (final NoSuchPrincipalException e) {
    568                    throw new ServletException(e);
    569            }
    570        }
    571
    572        /**
    573         *  Directly returns the UserProfile object attached to an uid.
    574         *
    575         *  @param uid The user id (e.g. WikiName)
    576         *  @return A UserProfile object
    577         *  @throws NoSuchPrincipalException If such a name does not exist.
    578         */
    579        public UserProfile getUserInfo( final String uid ) throws NoSuchPrincipalException {
    580            if( m_manager != null ) {
    581                return m_manager.getUserDatabase().findByUid( uid );
    582            }
    583
    584            throw new IllegalStateException( "The manager is offline." );
    585        }
    586    }
    587
    588}