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