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.log4j.Logger;
023import org.apache.wiki.WikiContext;
024import org.apache.wiki.WikiEngine;
025import org.apache.wiki.WikiSession;
026import org.apache.wiki.ajax.AjaxUtil;
027import org.apache.wiki.ajax.WikiAjaxDispatcherServlet;
028import org.apache.wiki.ajax.WikiAjaxServlet;
029import org.apache.wiki.api.engine.FilterManager;
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.AbstractUserDatabase;
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.SpamFilter;
043import org.apache.wiki.i18n.InternationalizationManager;
044import org.apache.wiki.preferences.Preferences;
045import org.apache.wiki.ui.InputValidator;
046import org.apache.wiki.util.ClassUtil;
047import org.apache.wiki.workflow.Decision;
048import org.apache.wiki.workflow.DecisionRequiredException;
049import org.apache.wiki.workflow.Fact;
050import org.apache.wiki.workflow.Step;
051import org.apache.wiki.workflow.Workflow;
052import org.apache.wiki.workflow.WorkflowBuilder;
053import org.apache.wiki.workflow.WorkflowManager;
054
055import javax.servlet.ServletException;
056import javax.servlet.http.HttpServletRequest;
057import javax.servlet.http.HttpServletResponse;
058import java.io.IOException;
059import java.security.Permission;
060import java.security.Principal;
061import java.text.MessageFormat;
062import java.util.List;
063import java.util.Map;
064import java.util.Properties;
065import java.util.ResourceBundle;
066import java.util.WeakHashMap;
067
068
069/**
070 * Provides a facade for obtaining user information.
071 * @since 2.3
072 */
073public class UserManager {
074
075    private static final String USERDATABASE_PACKAGE = "org.apache.wiki.auth.user";
076    private static final String SESSION_MESSAGES = "profile";
077    private static final String PARAM_EMAIL = "email";
078    private static final String PARAM_FULLNAME = "fullname";
079    private static final String PARAM_PASSWORD = "password";
080    private static final String PARAM_LOGINNAME = "loginname";
081    private static final String UNKNOWN_CLASS = "<unknown>";
082
083    private WikiEngine m_engine;
084
085    private static final Logger log = Logger.getLogger(UserManager.class);
086
087    /** Message key for the "save profile" message. */
088    private static final String PROP_DATABASE               = "jspwiki.userdatabase";
089
090    public static final String JSON_USERS = "users";
091
092    // private static final String  PROP_ACLMANAGER     = "jspwiki.aclManager";
093
094    /** Associates wiki sessions with profiles */
095    private final Map<WikiSession,UserProfile> m_profiles = new WeakHashMap<>();
096
097    /** The user database loads, manages and persists user identities */
098    private UserDatabase     m_database;
099
100    /**
101     * Initializes the engine for its nefarious purposes.
102     * @param engine the current wiki engine
103     * @param props the wiki engine initialization properties
104     */
105    public void initialize( WikiEngine engine, Properties props )
106    {
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.getPageManager() );
112
113        //TODO: Replace with custom annotations. See JSPWIKI-566
114        WikiAjaxDispatcherServlet.registerServlet( JSON_USERS, new JSONUserModule(this), new AllPermission(null));
115    }
116
117    /**
118     * Returns the UserDatabase employed by this WikiEngine. The UserDatabase is
119     * lazily initialized by this method, if it does not exist yet. If the
120     * initialization fails, this method will use the inner class
121     * DummyUserDatabase as a default (which is enough to get JSPWiki running).
122     * @return the dummy user database
123     * @since 2.3
124     */
125    public UserDatabase getUserDatabase()
126    {
127        // FIXME: Must not throw RuntimeException, but something else.
128        if( m_database != null )
129        {
130            return m_database;
131        }
132
133        String dbClassName = UNKNOWN_CLASS;
134
135        try
136        {
137            dbClassName = m_engine.getRequiredProperty( m_engine.getWikiProperties(), PROP_DATABASE );
138
139            log.info("Attempting to load user database class "+dbClassName);
140            final Class<?> dbClass = ClassUtil.findClass( USERDATABASE_PACKAGE, dbClassName );
141            m_database = (UserDatabase) dbClass.newInstance();
142            m_database.initialize( m_engine, m_engine.getWikiProperties() );
143            log.info("UserDatabase initialized.");
144        }
145        catch( final NoRequiredPropertyException e )
146        {
147            log.error( "You have not set the '"+PROP_DATABASE+"'. You need to do this if you want to enable user management by JSPWiki.", e );
148        }
149        catch( final ClassNotFoundException e )
150        {
151            log.error( "UserDatabase class " + dbClassName + " cannot be found", e );
152        }
153        catch( final InstantiationException e )
154        {
155            log.error( "UserDatabase class " + dbClassName + " cannot be created", e );
156        }
157        catch( final IllegalAccessException e )
158        {
159            log.error( "You are not allowed to access this user database class", e );
160        }
161        catch( final WikiSecurityException e )
162        {
163            log.error( "Exception initializing user database: " + e.getMessage(), e );
164        }
165        finally
166        {
167            if( m_database == null )
168            {
169                log.info("I could not create a database object you specified (or didn't specify), so I am falling back to a default.");
170                m_database = new DummyUserDatabase();
171            }
172        }
173
174        return m_database;
175    }
176
177    /**
178     * <p>Retrieves the {@link org.apache.wiki.auth.user.UserProfile}for the
179     * user in a wiki session. If the user is authenticated, the UserProfile
180     * returned will be the one stored in the user database; if one does not
181     * exist, a new one will be initialized and returned. If the user is
182     * anonymous or asserted, the UserProfile will <i>always</i> be newly
183     * initialized to prevent spoofing of identities. If a UserProfile needs to
184     * be initialized, its
185     * {@link org.apache.wiki.auth.user.UserProfile#isNew()} method will
186     * return <code>true</code>, and its login name will will be set
187     * automatically if the user is authenticated. Note that this method does
188     * not modify the retrieved (or newly created) profile otherwise; other
189     * fields in the user profile may be <code>null</code>.</p>
190     * <p>If a new UserProfile was created, but its
191     * {@link org.apache.wiki.auth.user.UserProfile#isNew()} method returns
192     * <code>false</code>, this method throws an {@link IllegalStateException}.
193     * This is meant as a quality check for UserDatabase providers;
194     * it should only be thrown if the implementation is faulty.</p>
195     * @param session the wiki session, which may not be <code>null</code>
196     * @return the user's profile, which will be newly initialized if the user
197     * is anonymous or asserted, or if the user cannot be found in the user
198     * database
199     */
200    public UserProfile getUserProfile( WikiSession session )
201    {
202        // Look up cached user profile
203        UserProfile profile = m_profiles.get( session );
204        boolean newProfile = profile == null;
205        Principal user = null;
206
207        // If user is authenticated, figure out if this is an existing profile
208        if ( session.isAuthenticated() )
209        {
210            user = session.getUserPrincipal();
211            try
212            {
213                profile = getUserDatabase().find( user.getName() );
214                newProfile = false;
215            }
216            catch( final NoSuchPrincipalException e )
217            {
218            }
219        }
220
221        if ( newProfile )
222        {
223            profile = getUserDatabase().newProfile();
224            if ( user != null )
225            {
226                profile.setLoginName( user.getName() );
227            }
228            if ( !profile.isNew() )
229            {
230                throw new IllegalStateException(
231                        "New profile should be marked 'new'. Check your UserProfile implementation." );
232            }
233        }
234
235        // Stash the profile for next time
236        m_profiles.put( session, profile );
237        return profile;
238    }
239
240    /**
241     * <p>
242     * Saves the {@link org.apache.wiki.auth.user.UserProfile}for the user in
243     * a wiki session. This method verifies that a user profile to be saved
244     * doesn't collide with existing profiles; that is, the login name
245     * or full name is already used by another profile. If the profile
246     * collides, a <code>DuplicateUserException</code> is thrown. After saving
247     * the profile, the user database changes are committed, and the user's
248     * credential set is refreshed; if custom authentication is used, this means
249     * the user will be automatically be logged in.
250     * </p>
251     * <p>
252     * When the user's profile is saved successfully, this method fires a
253     * {@link WikiSecurityEvent#PROFILE_SAVE} event with the WikiSession as the
254     * source and the UserProfile as target. For existing profiles, if the
255     * user's full name changes, this method also fires a "name changed"
256     * event ({@link WikiSecurityEvent#PROFILE_NAME_CHANGED}) with the
257     * WikiSession as the source and an array containing the old and new
258     * UserProfiles, respectively. The <code>NAME_CHANGED</code> event allows
259     * the GroupManager and PageManager can change group memberships and
260     * ACLs if needed.
261     * </p>
262     * <p>
263     * Note that WikiSessions normally attach event listeners to the
264     * UserManager, so changes to the profile will automatically cause the
265     * correct Principals to be reloaded into the current WikiSession's Subject.
266     * </p>
267     * @param session the wiki session, which may not be <code>null</code>
268     * @param profile the user profile, which may not be <code>null</code>
269     * @throws DuplicateUserException if the proposed profile's login name or full name collides with another
270     * @throws WikiException if the save fails for some reason. If the current user does not have
271     * permission to save the profile, this will be a {@link org.apache.wiki.auth.WikiSecurityException};
272     * if if the user profile must be approved before it can be saved, it will be a
273     * {@link org.apache.wiki.workflow.DecisionRequiredException}. All other WikiException
274     * indicate a condition that is not normal is probably due to mis-configuration
275     */
276    public void setUserProfile( WikiSession session, UserProfile profile ) throws DuplicateUserException, WikiException
277    {
278        // Verify user is allowed to save profile!
279        final Permission p = new WikiPermission( m_engine.getApplicationName(), WikiPermission.EDIT_PROFILE_ACTION );
280        if ( !m_engine.getAuthorizationManager().checkPermission( session, p ) )
281        {
282            throw new WikiSecurityException( "You are not allowed to save wiki profiles." );
283        }
284
285        // Check if profile is new, and see if container allows creation
286        final boolean newProfile = profile.isNew();
287
288        // Check if another user profile already has the fullname or loginname
289        final UserProfile oldProfile = getUserProfile( session );
290        final boolean nameChanged = ( oldProfile == null  || oldProfile.getFullname() == null )
291            ? false
292            : !( oldProfile.getFullname().equals( profile.getFullname() ) &&
293                 oldProfile.getLoginName().equals( profile.getLoginName() ) );
294        UserProfile otherProfile;
295        try
296        {
297            otherProfile = getUserDatabase().findByLoginName( profile.getLoginName() );
298            if ( otherProfile != null && !otherProfile.equals( oldProfile ) )
299            {
300                throw new DuplicateUserException( "security.error.login.taken", profile.getLoginName() );
301            }
302        }
303        catch( final NoSuchPrincipalException e )
304        {
305        }
306        try
307        {
308            otherProfile = getUserDatabase().findByFullName( profile.getFullname() );
309            if ( otherProfile != null && !otherProfile.equals( oldProfile ) )
310            {
311                throw new DuplicateUserException( "security.error.fullname.taken", profile.getFullname() );
312            }
313        }
314        catch( final NoSuchPrincipalException e )
315        {
316        }
317
318        // For new accounts, create approval workflow for user profile save.
319        if ( newProfile && oldProfile != null && oldProfile.isNew() )
320        {
321            startUserProfileCreationWorkflow(session, profile);
322
323            // If the profile doesn't need approval, then just log the user in
324
325            try
326            {
327                final AuthenticationManager mgr = m_engine.getAuthenticationManager();
328                if ( newProfile && !mgr.isContainerAuthenticated() )
329                {
330                    mgr.login( session, null, profile.getLoginName(), profile.getPassword() );
331                }
332            }
333            catch ( final WikiException e )
334            {
335                throw new WikiSecurityException( e.getMessage(), e );
336            }
337
338            // Alert all listeners that the profile changed...
339            // ...this will cause credentials to be reloaded in the wiki session
340            fireEvent( WikiSecurityEvent.PROFILE_SAVE, session, profile );
341        }
342
343        // For existing accounts, just save the profile
344        else
345        {
346            // If login name changed, rename it first
347            if ( nameChanged && oldProfile != null && !oldProfile.getLoginName().equals( profile.getLoginName() ) )
348            {
349                getUserDatabase().rename( oldProfile.getLoginName(), profile.getLoginName() );
350            }
351
352            // Now, save the profile (userdatabase will take care of timestamps for us)
353            getUserDatabase().save( profile );
354
355            if ( nameChanged )
356            {
357                // Fire an event if the login name or full name changed
358                final UserProfile[] profiles = new UserProfile[] { oldProfile, profile };
359                fireEvent( WikiSecurityEvent.PROFILE_NAME_CHANGED, session, profiles );
360            }
361            else
362            {
363                // Fire an event that says we have new a new profile (new principals)
364                fireEvent( WikiSecurityEvent.PROFILE_SAVE, session, profile );
365            }
366        }
367    }
368
369    public void startUserProfileCreationWorkflow( final WikiSession session, final UserProfile profile ) throws WikiException {
370        final WorkflowBuilder builder = WorkflowBuilder.getBuilder( m_engine );
371        final Principal submitter = session.getUserPrincipal();
372        final Step completionTask = m_engine.getTasksManager().buildSaveUserProfileTask( m_engine, session.getLocale() );
373
374        // Add user profile attribute as Facts for the approver (if required)
375        final boolean hasEmail = profile.getEmail() != null;
376        final Fact[] facts = new Fact[ hasEmail ? 4 : 3];
377        facts[0] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_PREFS_FULL_NAME, profile.getFullname() );
378        facts[1] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_PREFS_LOGIN_NAME, profile.getLoginName() );
379        facts[2] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_SUBMITTER, submitter.getName() );
380        if ( hasEmail )
381        {
382            facts[3] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_PREFS_EMAIL, profile.getEmail() );
383        }
384        final Workflow workflow = builder.buildApprovalWorkflow( submitter, 
385                                                                 WorkflowManager.WF_UP_CREATE_SAVE_APPROVER, 
386                                                                 null, 
387                                                                 WorkflowManager.WF_UP_CREATE_SAVE_DECISION_MESSAGE_KEY, 
388                                                                 facts, 
389                                                                 completionTask, 
390                                                                 null );
391
392        workflow.setAttribute( WorkflowManager.WF_UP_CREATE_SAVE_ATTR_SAVED_PROFILE, profile );
393        m_engine.getWorkflowManager().start(workflow);
394
395        final boolean approvalRequired = workflow.getCurrentStep() instanceof Decision;
396
397        // If the profile requires approval, redirect user to message page
398        if ( approvalRequired )
399        {
400            throw new DecisionRequiredException( "This profile must be approved before it becomes active" );
401        }
402    }
403
404    /**
405     * <p> Extracts user profile parameters from the HTTP request and populates
406     * a UserProfile with them. The UserProfile will either be a copy of the
407     * user's existing profile (if one can be found), or a new profile (if not).
408     * The rules for populating the profile as as follows: </p> <ul> <li>If the
409     * <code>email</code> or <code>password</code> parameter values differ
410     * from those in the existing profile, the passed parameters override the
411     * old values.</li> <li>For new profiles, the user-supplied
412     * <code>fullname</code> parameter is always
413     * used; for existing profiles the existing value is used, and whatever
414     * value the user supplied is discarded. The wiki name is automatically
415     * computed by taking the full name and extracting all whitespace.</li>
416     * <li>In all cases, the
417     * created/last modified timestamps of the user's existing or new profile
418     * always override whatever values the user supplied.</li> <li>If
419     * container authentication is used, the login name property of the profile
420     * is set to the name of
421     * {@link org.apache.wiki.WikiSession#getLoginPrincipal()}. Otherwise,
422     * the value of the <code>loginname</code> parameter is used.</li> </ul>
423     * @param context the current wiki context
424     * @return a new, populated user profile
425     */
426    public UserProfile parseProfile( WikiContext context )
427    {
428        // Retrieve the user's profile (may have been previously cached)
429        final UserProfile profile = getUserProfile( context.getWikiSession() );
430        final HttpServletRequest request = context.getHttpRequest();
431
432        // Extract values from request stream (cleanse whitespace as needed)
433        String loginName = request.getParameter( PARAM_LOGINNAME );
434        String password = request.getParameter( PARAM_PASSWORD );
435        String fullname = request.getParameter( PARAM_FULLNAME );
436        String email = request.getParameter( PARAM_EMAIL );
437        loginName = InputValidator.isBlank( loginName ) ? null : loginName;
438        password = InputValidator.isBlank( password ) ? null : password;
439        fullname = InputValidator.isBlank( fullname ) ? null : fullname;
440        email = InputValidator.isBlank( email ) ? null : email;
441
442        // A special case if we have container authentication
443        // If authenticated, login name is always taken from container
444        if ( m_engine.getAuthenticationManager().isContainerAuthenticated() &&
445                context.getWikiSession().isAuthenticated() )
446        {
447            loginName = context.getWikiSession().getLoginPrincipal().getName();
448        }
449
450        // Set the profile fields!
451        profile.setLoginName( loginName );
452        profile.setEmail( email );
453        profile.setFullname( fullname );
454        profile.setPassword( password );
455        return profile;
456    }
457
458    /**
459     * Validates a user profile, and appends any errors to the session errors
460     * list. If the profile is new, the password will be checked to make sure it
461     * isn't null. Otherwise, the password is checked for length and that it
462     * matches the value of the 'password2' HTTP parameter. Note that we have a
463     * special case when container-managed authentication is used and the user
464     * is not authenticated; this will always cause validation to fail. Any
465     * validation errors are added to the wiki session's messages collection
466     * (see {@link WikiSession#getMessages()}.
467     * @param context the current wiki context
468     * @param profile the supplied UserProfile
469     */
470    public void validateProfile( WikiContext context, UserProfile profile )
471    {
472        final boolean isNew = profile.isNew();
473        final WikiSession session = context.getWikiSession();
474        final InputValidator validator = new InputValidator( SESSION_MESSAGES, context );
475        final ResourceBundle rb = Preferences.getBundle( context, InternationalizationManager.CORE_BUNDLE );
476
477        //
478        //  Query the SpamFilter first
479        //
480        final FilterManager fm = m_engine.getFilterManager();
481        final List<PageFilter> ls = fm.getFilterList();
482        for( final PageFilter pf : ls )
483        {
484            if( pf instanceof SpamFilter )
485            {
486                if( ((SpamFilter)pf).isValidUserProfile( context, profile ) == false )
487                {
488                    session.addMessage( SESSION_MESSAGES, "Invalid userprofile" );
489                    return;
490                }
491                break;
492            }
493        }
494
495        // If container-managed auth and user not logged in, throw an error
496        if ( m_engine.getAuthenticationManager().isContainerAuthenticated()
497             && !context.getWikiSession().isAuthenticated() )
498        {
499            session.addMessage( SESSION_MESSAGES, rb.getString("security.error.createprofilebeforelogin") );
500        }
501
502        validator.validateNotNull( profile.getLoginName(), rb.getString("security.user.loginname") );
503        validator.validateNotNull( profile.getFullname(), rb.getString("security.user.fullname") );
504        validator.validate( profile.getEmail(), rb.getString("security.user.email"), InputValidator.EMAIL );
505
506        // If new profile, passwords must match and can't be null
507        if ( !m_engine.getAuthenticationManager().isContainerAuthenticated() )
508        {
509            final String password = profile.getPassword();
510            if ( password == null )
511            {
512                if ( isNew )
513                {
514                    session.addMessage( SESSION_MESSAGES, rb.getString("security.error.blankpassword") );
515                }
516            }
517            else
518            {
519                final HttpServletRequest request = context.getHttpRequest();
520                final String password2 = ( request == null ) ? null : request.getParameter( "password2" );
521                if ( !password.equals( password2 ) )
522                {
523                    session.addMessage( SESSION_MESSAGES, rb.getString("security.error.passwordnomatch") );
524                }
525            }
526        }
527
528        UserProfile otherProfile;
529        final String fullName = profile.getFullname();
530        final String loginName = profile.getLoginName();
531        final String email = profile.getEmail();
532
533        // It's illegal to use as a full name someone else's login name
534        try
535        {
536            otherProfile = getUserDatabase().find( fullName );
537            if ( otherProfile != null && !profile.equals( otherProfile ) && !fullName.equals( otherProfile.getFullname() ) )
538            {
539                final Object[] args = { fullName };
540                session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString("security.error.illegalfullname"), args ) );
541            }
542        }
543        catch ( final NoSuchPrincipalException e)
544        { /* It's clean */ }
545
546        // It's illegal to use as a login name someone else's full name
547        try
548        {
549            otherProfile = getUserDatabase().find( loginName );
550            if ( otherProfile != null && !profile.equals( otherProfile ) && !loginName.equals( otherProfile.getLoginName() ) )
551            {
552                final Object[] args = { loginName };
553                session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString("security.error.illegalloginname"), args ) );
554            }
555        }
556        catch ( final NoSuchPrincipalException e)
557        { /* It's clean */ }
558
559        // It's illegal to use multiple accounts with the same email
560        try
561        {
562            otherProfile = getUserDatabase().findByEmail( email );
563            if ( otherProfile != null
564                && !profile.getUid().equals(otherProfile.getUid()) // Issue JSPWIKI-1042
565                && !profile.equals( otherProfile ) && StringUtils.lowerCase( email ).equals( StringUtils.lowerCase(otherProfile.getEmail() ) ) )
566            {
567                final Object[] args = { email };
568                session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString("security.error.email.taken"), args ) );
569            }
570        }
571        catch ( final NoSuchPrincipalException e)
572        { /* It's clean */ }
573    }
574
575    /**
576     *  A helper method for returning all of the known WikiNames in this system.
577     *
578     *  @return An Array of Principals
579     *  @throws WikiSecurityException If for reason the names cannot be fetched
580     */
581    public Principal[] listWikiNames()
582        throws WikiSecurityException
583    {
584        return getUserDatabase().getWikiNames();
585    }
586
587    /**
588     * This is a database that gets used if nothing else is available. It does
589     * nothing of note - it just mostly throws NoSuchPrincipalExceptions if
590     * someone tries to log in.
591     */
592    public static class DummyUserDatabase extends AbstractUserDatabase
593    {
594
595        /**
596         * No-op.
597         * @param loginName the login name to delete
598         * @throws WikiSecurityException never...
599         */
600        @Override
601        public void deleteByLoginName( String loginName ) throws WikiSecurityException
602        {
603            // No operation
604        }
605
606        /**
607         * No-op; always throws <code>NoSuchPrincipalException</code>.
608         * @param index the name to search for
609         * @return the user profile
610         * @throws NoSuchPrincipalException never...
611         */
612        @Override
613        public UserProfile findByEmail(String index) throws NoSuchPrincipalException
614        {
615            throw new NoSuchPrincipalException("No user profiles available");
616        }
617
618        /**
619         * No-op; always throws <code>NoSuchPrincipalException</code>.
620         * @param index the name to search for
621         * @return the user profile
622         * @throws NoSuchPrincipalException never...
623         */
624        @Override
625        public UserProfile findByFullName(String index) throws NoSuchPrincipalException
626        {
627            throw new NoSuchPrincipalException("No user profiles available");
628        }
629
630        /**
631         * No-op; always throws <code>NoSuchPrincipalException</code>.
632         * @param index the name to search for
633         * @return the user profile
634         * @throws NoSuchPrincipalException never...
635         */
636        @Override
637        public UserProfile findByLoginName(String index) throws NoSuchPrincipalException
638        {
639            throw new NoSuchPrincipalException("No user profiles available");
640        }
641
642        /**
643         * No-op; always throws <code>NoSuchPrincipalException</code>.
644         * @param uid the unique identifier to search for
645         * @return the user profile
646         * @throws NoSuchPrincipalException never...
647         */
648        @Override
649        public UserProfile findByUid( String uid ) throws NoSuchPrincipalException
650        {
651            throw new NoSuchPrincipalException("No user profiles available");
652        }
653        /**
654         * No-op; always throws <code>NoSuchPrincipalException</code>.
655         * @param index the name to search for
656         * @return the user profile
657         * @throws NoSuchPrincipalException never...
658         */
659        @Override
660        public UserProfile findByWikiName(String index) throws NoSuchPrincipalException
661        {
662            throw new NoSuchPrincipalException("No user profiles available");
663        }
664
665        /**
666         * No-op.
667         * @return a zero-length array
668         * @throws WikiSecurityException never...
669         */
670        @Override
671        public Principal[] getWikiNames() throws WikiSecurityException
672        {
673            return new Principal[0];
674        }
675
676        /**
677         * No-op.
678         *
679         * @param engine the wiki engine
680         * @param props the properties used to initialize the wiki engine
681         * @throws NoRequiredPropertyException never...
682         */
683        @Override
684        public void initialize(WikiEngine engine, Properties props) throws NoRequiredPropertyException
685        {
686        }
687
688        /**
689         * No-op; always throws <code>NoSuchPrincipalException</code>.
690         * @param loginName the login name
691         * @param newName the proposed new login name
692         * @throws DuplicateUserException never...
693         * @throws WikiSecurityException never...
694         */
695        @Override
696        public void rename( String loginName, String newName ) throws DuplicateUserException, WikiSecurityException
697        {
698            throw new NoSuchPrincipalException("No user profiles available");
699        }
700
701        /**
702         * No-op.
703         * @param profile the user profile
704         * @throws WikiSecurityException never...
705         */
706        @Override
707        public void save( UserProfile profile ) throws WikiSecurityException
708        {
709        }
710
711    }
712
713    // events processing .......................................................
714
715    /**
716     * Registers a WikiEventListener with this instance.
717     * This is a convenience method.
718     * @param listener the event listener
719     */
720    public synchronized void addWikiEventListener( WikiEventListener listener )
721    {
722        WikiEventManager.addWikiEventListener( this, listener );
723    }
724
725    /**
726     * Un-registers a WikiEventListener with this instance.
727     * This is a convenience method.
728     * @param listener the event listener
729     */
730    public synchronized void removeWikiEventListener( WikiEventListener listener )
731    {
732        WikiEventManager.removeWikiEventListener( this, listener );
733    }
734
735    /**
736     *  Fires a WikiSecurityEvent of the provided type, Principal and target Object
737     *  to all registered listeners.
738     *
739     * @see org.apache.wiki.event.WikiSecurityEvent
740     * @param type       the event type to be fired
741     * @param session    the wiki session supporting the event
742     * @param profile    the user profile (or array of user profiles), which may be <code>null</code>
743     */
744    protected void fireEvent( int type, WikiSession session, Object profile )
745    {
746        if ( WikiEventManager.isListening(this) )
747        {
748            WikiEventManager.fireEvent(this,new WikiSecurityEvent(session,type,profile));
749        }
750    }
751
752    /**
753     *  Implements the JSON API for usermanager.
754     *  <p>
755     *  Even though this gets serialized whenever container shuts down/restarts,
756     *  this gets reinstalled to the session when JSPWiki starts.  This means
757     *  that it's not actually necessary to save anything.
758     */
759    public static final class JSONUserModule implements WikiAjaxServlet
760    {
761        private volatile UserManager m_manager;
762
763        /**
764         *  Create a new JSONUserModule.
765         *  @param mgr Manager
766         */
767        public JSONUserModule( UserManager mgr )
768        {
769            m_manager = mgr;
770        }
771
772        @Override
773        public String getServletMapping() {
774            return JSON_USERS;
775        }
776
777        @Override
778        public void service(HttpServletRequest req, HttpServletResponse resp, String actionName, List<String> params) throws ServletException, IOException {
779            try {
780                String uid = null;
781                if (params.size()<1) {
782                    return;
783                }
784                uid = params.get(0);
785                log.debug("uid="+uid);
786                if (StringUtils.isNotBlank(uid)) {
787                    final UserProfile prof = getUserInfo(uid);
788                    resp.getWriter().write(AjaxUtil.toJson(prof));
789                }
790            } catch (final NoSuchPrincipalException e) {
791                throw new ServletException(e);
792            }
793        }
794
795        /**
796         *  Directly returns the UserProfile object attached to an uid.
797         *
798         *  @param uid The user id (e.g. WikiName)
799         *  @return A UserProfile object
800         *  @throws NoSuchPrincipalException If such a name does not exist.
801         */
802        public UserProfile getUserInfo( String uid )
803            throws NoSuchPrincipalException
804        {
805            if( m_manager != null )
806            {
807                final UserProfile prof = m_manager.getUserDatabase().find( uid );
808
809                return prof;
810            }
811
812            throw new IllegalStateException("The manager is offline.");
813        }
814    }
815}