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            final 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( final 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( final ClassNotFoundException e )
172        {
173            log.error( "UserDatabase class " + dbClassName + " cannot be found", e );
174        }
175        catch( final InstantiationException e )
176        {
177            log.error( "UserDatabase class " + dbClassName + " cannot be created", e );
178        }
179        catch( final IllegalAccessException e )
180        {
181            log.error( "You are not allowed to access this user database class", e );
182        }
183        catch( final 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( final 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        final 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        final boolean newProfile = profile.isNew();
309
310        // Check if another user profile already has the fullname or loginname
311        final UserProfile oldProfile = getUserProfile( session );
312        final 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( final 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( final 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            final WorkflowBuilder builder = WorkflowBuilder.getBuilder( m_engine );
344            final Principal submitter = session.getUserPrincipal();
345            final Task completionTask = new SaveUserProfileTask( m_engine, session.getLocale() );
346
347            // Add user profile attribute as Facts for the approver (if required)
348            final boolean hasEmail = profile.getEmail() != null;
349            final 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            final 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            final 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                final AuthenticationManager mgr = m_engine.getAuthenticationManager();
381                if ( newProfile && !mgr.isContainerAuthenticated() )
382                {
383                    mgr.login( session, null, profile.getLoginName(), profile.getPassword() );
384                }
385            }
386            catch ( final 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                final 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        final UserProfile profile = getUserProfile( context.getWikiSession() );
448        final 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        final boolean isNew = profile.isNew();
491        final WikiSession session = context.getWikiSession();
492        final InputValidator validator = new InputValidator( SESSION_MESSAGES, context );
493        final ResourceBundle rb = Preferences.getBundle( context, InternationalizationManager.CORE_BUNDLE );
494
495        //
496        //  Query the SpamFilter first
497        //
498        final FilterManager fm = m_engine.getFilterManager();
499        final List<PageFilter> ls = fm.getFilterList();
500        for( final 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            final 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                final HttpServletRequest request = context.getHttpRequest();
538                final 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        final String fullName = profile.getFullname();
548        final String loginName = profile.getLoginName();
549        final 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                final Object[] args = { fullName };
558                session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString("security.error.illegalfullname"), args ) );
559            }
560        }
561        catch ( final NoSuchPrincipalException e)
562        { /* It's clean */ }
563
564        // It's illegal to use as a login name someone else's full name
565        try
566        {
567            otherProfile = getUserDatabase().find( loginName );
568            if ( otherProfile != null && !profile.equals( otherProfile ) && !loginName.equals( otherProfile.getLoginName() ) )
569            {
570                final Object[] args = { loginName };
571                session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString("security.error.illegalloginname"), args ) );
572            }
573        }
574        catch ( final NoSuchPrincipalException e)
575        { /* It's clean */ }
576
577        // It's illegal to use multiple accounts with the same email
578        try
579        {
580            otherProfile = getUserDatabase().findByEmail( email );
581            if ( otherProfile != null
582                && !profile.getUid().equals(otherProfile.getUid()) // Issue JSPWIKI-1042
583                && !profile.equals( otherProfile ) && StringUtils.lowerCase( email ).equals( StringUtils.lowerCase(otherProfile.getEmail() ) ) )
584            {
585                final Object[] args = { email };
586                session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString("security.error.email.taken"), args ) );
587            }
588        }
589        catch ( final 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        @Override
618        public void commit() throws WikiSecurityException
619        {
620            // No operation
621        }
622
623        /**
624         * No-op.
625         * @param loginName the login name to delete
626         * @throws WikiSecurityException never...
627         */
628        @Override
629        public void deleteByLoginName( String loginName ) throws WikiSecurityException
630        {
631            // No operation
632        }
633
634        /**
635         * No-op; always throws <code>NoSuchPrincipalException</code>.
636         * @param index the name to search for
637         * @return the user profile
638         * @throws NoSuchPrincipalException never...
639         */
640        @Override
641        public UserProfile findByEmail(String index) throws NoSuchPrincipalException
642        {
643            throw new NoSuchPrincipalException("No user profiles available");
644        }
645
646        /**
647         * No-op; always throws <code>NoSuchPrincipalException</code>.
648         * @param index the name to search for
649         * @return the user profile
650         * @throws NoSuchPrincipalException never...
651         */
652        @Override
653        public UserProfile findByFullName(String index) throws NoSuchPrincipalException
654        {
655            throw new NoSuchPrincipalException("No user profiles available");
656        }
657
658        /**
659         * No-op; always throws <code>NoSuchPrincipalException</code>.
660         * @param index the name to search for
661         * @return the user profile
662         * @throws NoSuchPrincipalException never...
663         */
664        @Override
665        public UserProfile findByLoginName(String index) throws NoSuchPrincipalException
666        {
667            throw new NoSuchPrincipalException("No user profiles available");
668        }
669
670        /**
671         * No-op; always throws <code>NoSuchPrincipalException</code>.
672         * @param uid the unique identifier to search for
673         * @return the user profile
674         * @throws NoSuchPrincipalException never...
675         */
676        @Override
677        public UserProfile findByUid( String uid ) throws NoSuchPrincipalException
678        {
679            throw new NoSuchPrincipalException("No user profiles available");
680        }
681        /**
682         * No-op; always throws <code>NoSuchPrincipalException</code>.
683         * @param index the name to search for
684         * @return the user profile
685         * @throws NoSuchPrincipalException never...
686         */
687        @Override
688        public UserProfile findByWikiName(String index) throws NoSuchPrincipalException
689        {
690            throw new NoSuchPrincipalException("No user profiles available");
691        }
692
693        /**
694         * No-op.
695         * @return a zero-length array
696         * @throws WikiSecurityException never...
697         */
698        @Override
699        public Principal[] getWikiNames() throws WikiSecurityException
700        {
701            return new Principal[0];
702        }
703
704        /**
705         * No-op.
706         *
707         * @param engine the wiki engine
708         * @param props the properties used to initialize the wiki engine
709         * @throws NoRequiredPropertyException never...
710         */
711        @Override
712        public void initialize(WikiEngine engine, Properties props) throws NoRequiredPropertyException
713        {
714        }
715
716        /**
717         * No-op; always throws <code>NoSuchPrincipalException</code>.
718         * @param loginName the login name
719         * @param newName the proposed new login name
720         * @throws DuplicateUserException never...
721         * @throws WikiSecurityException never...
722         */
723        @Override
724        public void rename( String loginName, String newName ) throws DuplicateUserException, WikiSecurityException
725        {
726            throw new NoSuchPrincipalException("No user profiles available");
727        }
728
729        /**
730         * No-op.
731         * @param profile the user profile
732         * @throws WikiSecurityException never...
733         */
734        @Override
735        public void save( UserProfile profile ) throws WikiSecurityException
736        {
737        }
738
739    }
740
741    // workflow task inner classes....................................................
742
743    /**
744     * Inner class that handles the actual profile save action. Instances
745     * of this class are assumed to have been added to an approval workflow via
746     * {@link org.apache.wiki.workflow.WorkflowBuilder#buildApprovalWorkflow(Principal, String, Task, String, org.apache.wiki.workflow.Fact[], Task, String)};
747     * they will not function correctly otherwise.
748     *
749     */
750    public static class SaveUserProfileTask extends Task
751    {
752        private static final long serialVersionUID = 6994297086560480285L;
753        private final UserDatabase m_db;
754        private final WikiEngine m_engine;
755        private final Locale m_loc;
756
757        /**
758         * Constructs a new Task for saving a user profile.
759         * @param engine the wiki engine
760         * @deprecated will be removed in 2.10 scope. Consider using
761         * {@link #UserManager.SaveUserProfileTask(WikiEngine, Locale)} instead
762         */
763        @Deprecated
764        public SaveUserProfileTask( WikiEngine engine )
765        {
766            super( SAVE_TASK_MESSAGE_KEY );
767            m_engine = engine;
768            m_db = engine.getUserManager().getUserDatabase();
769            m_loc = null;
770        }
771
772        public SaveUserProfileTask( WikiEngine engine, Locale loc )
773        {
774            super( SAVE_TASK_MESSAGE_KEY );
775            m_engine = engine;
776            m_db = engine.getUserManager().getUserDatabase();
777            m_loc = loc;
778        }
779
780        /**
781         * Saves the user profile to the user database.
782         * @return {@link org.apache.wiki.workflow.Outcome#STEP_COMPLETE} if the
783         * task completed successfully
784         * @throws WikiException if the save did not complete for some reason
785         */
786        @Override
787        public Outcome execute() throws WikiException
788        {
789            // Retrieve user profile
790            final UserProfile profile = (UserProfile) getWorkflow().getAttribute( SAVED_PROFILE );
791
792            // Save the profile (userdatabase will take care of timestamps for us)
793            m_db.save( profile );
794
795            // Send e-mail if user supplied an e-mail address
796            if ( profile.getEmail() != null )
797            {
798                try
799                {
800                    final InternationalizationManager i18n = m_engine.getInternationalizationManager();
801                    final String app = m_engine.getApplicationName();
802                    final String to = profile.getEmail();
803                    final String subject = i18n.get( InternationalizationManager.DEF_TEMPLATE, m_loc,
804                                               "notification.createUserProfile.accept.subject", app );
805
806                    final String content = i18n.get( InternationalizationManager.DEF_TEMPLATE, m_loc,
807                                               "notification.createUserProfile.accept.content", app,
808                                               profile.getLoginName(),
809                                               profile.getFullname(),
810                                               profile.getEmail(),
811                                               m_engine.getURL( WikiContext.LOGIN, null, null, true ) );
812                    MailUtil.sendMessage( m_engine.getWikiProperties(), to, subject, content);
813                }
814                catch ( final AddressException e)
815                {
816                }
817                catch ( final MessagingException me )
818                {
819                    log.error( "Could not send registration confirmation e-mail. Is the e-mail server running?", me );
820                }
821            }
822
823            return Outcome.STEP_COMPLETE;
824        }
825    }
826
827    // events processing .......................................................
828
829    /**
830     * Registers a WikiEventListener with this instance.
831     * This is a convenience method.
832     * @param listener the event listener
833     */
834    public synchronized void addWikiEventListener( WikiEventListener listener )
835    {
836        WikiEventManager.addWikiEventListener( this, listener );
837    }
838
839    /**
840     * Un-registers a WikiEventListener with this instance.
841     * This is a convenience method.
842     * @param listener the event listener
843     */
844    public synchronized void removeWikiEventListener( WikiEventListener listener )
845    {
846        WikiEventManager.removeWikiEventListener( this, listener );
847    }
848
849    /**
850     *  Fires a WikiSecurityEvent of the provided type, Principal and target Object
851     *  to all registered listeners.
852     *
853     * @see org.apache.wiki.event.WikiSecurityEvent
854     * @param type       the event type to be fired
855     * @param session    the wiki session supporting the event
856     * @param profile    the user profile (or array of user profiles), which may be <code>null</code>
857     */
858    protected void fireEvent( int type, WikiSession session, Object profile )
859    {
860        if ( WikiEventManager.isListening(this) )
861        {
862            WikiEventManager.fireEvent(this,new WikiSecurityEvent(session,type,profile));
863        }
864    }
865
866    /**
867     *  Implements the JSON API for usermanager.
868     *  <p>
869     *  Even though this gets serialized whenever container shuts down/restarts,
870     *  this gets reinstalled to the session when JSPWiki starts.  This means
871     *  that it's not actually necessary to save anything.
872     */
873    public static final class JSONUserModule implements WikiAjaxServlet
874    {
875        private volatile UserManager m_manager;
876
877        /**
878         *  Create a new JSONUserModule.
879         *  @param mgr Manager
880         */
881        public JSONUserModule( UserManager mgr )
882        {
883            m_manager = mgr;
884        }
885
886        @Override
887        public String getServletMapping() {
888            return JSON_USERS;
889        }
890
891        @Override
892        public void service(HttpServletRequest req, HttpServletResponse resp, String actionName, List<String> params) throws ServletException, IOException {
893            try {
894                String uid = null;
895                if (params.size()<1) {
896                    return;
897                }
898                uid = params.get(0);
899                log.debug("uid="+uid);
900                if (StringUtils.isNotBlank(uid)) {
901                    final UserProfile prof = getUserInfo(uid);
902                    resp.getWriter().write(AjaxUtil.toJson(prof));
903                }
904            } catch (final NoSuchPrincipalException e) {
905                throw new ServletException(e);
906            }
907        }
908
909        /**
910         *  Directly returns the UserProfile object attached to an uid.
911         *
912         *  @param uid The user id (e.g. WikiName)
913         *  @return A UserProfile object
914         *  @throws NoSuchPrincipalException If such a name does not exist.
915         */
916        public UserProfile getUserInfo( String uid )
917            throws NoSuchPrincipalException
918        {
919            if( m_manager != null )
920            {
921                final UserProfile prof = m_manager.getUserDatabase().find( uid );
922
923                return prof;
924            }
925
926            throw new IllegalStateException("The manager is offline.");
927        }
928    }
929}