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