/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.authenticate;

import java.sql.SQLException;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.dspace.authorize.AuthorizeException;

import org.dspace.core.Context;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.LogManager;
import org.dspace.authenticate.AuthenticationManager;
import org.dspace.authenticate.AuthenticationMethod;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;

/**
 * Shibboleth authentication for DSpace, tested on Shibboleth 1.3.x and
 * Shibboleth 2.x. Read <a href=
 * "https://mams.melcoe.mq.edu.au/zope/mams/pubs/Installation/dspace15/view"
 * >Shib DSpace 1.5</a> for installation procedure. Read dspace.cfg for details
 * on options available.
 * 
 * @author <a href="mailto:bliong@melcoe.mq.edu.au">Bruc Liong, MELCOE</a>
 * @author <a href="mailto:kli@melcoe.mq.edu.au">Xiang Kevin Li, MELCOE</a>
 * @version $Revision: 5844 $
 */
public class ShibAuthentication implements AuthenticationMethod
{
    /** log4j category */
    private static Logger log = Logger.getLogger(ShibAuthentication.class);

    public int authenticate(Context context, String username, String password,
            String realm, HttpServletRequest request) throws SQLException
    {
        if (request == null)
        {
            return BAD_ARGS;
        }
        log.info("Shibboleth login started...");

        java.util.Enumeration names = request.getHeaderNames();
        String name;
        while (names.hasMoreElements())
        {
            name = names.nextElement().toString();
            log.debug("header:" + name + "=" + request.getHeader(name));
        }

        boolean isUsingTomcatUser = ConfigurationManager.getBooleanProperty("authentication.shib.email-use-tomcat-remote-user");
        String emailHeader = ConfigurationManager.getProperty("authentication.shib.email-header");
        String fnameHeader = ConfigurationManager.getProperty("authentication.shib.firstname-header");
        String lnameHeader = ConfigurationManager.getProperty("authentication.shib.lastname-header");

        String email = null;
        String fname = null;
        String lname = null;

        if (emailHeader != null)
        {
            // try to grab email from the header
            email = request.getHeader(emailHeader);

            // fail, try lower case
            if (email == null)
            {
                email = request.getHeader(emailHeader.toLowerCase());
            }
        }

        // try to pull the "REMOTE_USER" info instead of the header
        if (email == null && isUsingTomcatUser)
        {
            email = request.getRemoteUser();
            log.info("RemoteUser identified as: " + email);
        }

        // No email address, perhaps the eperson has been setup, better check it
        if (email == null)
        {
            EPerson p = context.getCurrentUser();
            if (p != null)
            {
                email = p.getEmail();
            }
        }

        if (email == null)
        {
            log
                    .error("No email is given, you're denied access by Shib, please release email address");
            return AuthenticationMethod.BAD_ARGS;
        }

        email = email.toLowerCase();

        if (fnameHeader != null)
        {
            // try to grab name from the header
            fname = request.getHeader(fnameHeader);

            // fail, try lower case
            if (fname == null)
            {
                fname = request.getHeader(fnameHeader.toLowerCase());
            }
        }
        if (lnameHeader != null)
        {
            // try to grab name from the header
            lname = request.getHeader(lnameHeader);

            // fail, try lower case
            if (lname == null)
            {
                lname = request.getHeader(lnameHeader.toLowerCase());
            }
        }

        // future version can offer auto-update feature, this needs testing
        // before inclusion to core code

        EPerson eperson = null;
        try
        {
            eperson = EPerson.findByEmail(context, email);
            context.setCurrentUser(eperson);
        }
        catch (AuthorizeException e)
        {
            log.warn("Fail to locate user with email:" + email, e);
            eperson = null;
        }

        // auto create user if needed
        if (eperson == null
                && ConfigurationManager
                        .getBooleanProperty("authentication.shib.autoregister"))
        {
            log.info(LogManager.getHeader(context, "autoregister", "email="
                    + email));

            // TEMPORARILY turn off authorisation
            context.setIgnoreAuthorization(true);
            try
            {
                eperson = EPerson.create(context);
                eperson.setEmail(email);
                if (fname != null)
                {
                    eperson.setFirstName(fname);
                }
                if (lname != null)
                {
                    eperson.setLastName(lname);
                }
                eperson.setCanLogIn(true);
                AuthenticationManager.initEPerson(context, request, eperson);
                eperson.update();
                context.commit();
                context.setCurrentUser(eperson);
            }
            catch (AuthorizeException e)
            {
                log.warn("Fail to authorize user with email:" + email, e);
                eperson = null;
            }
            finally
            {
                context.setIgnoreAuthorization(false);
            }
        }

        if (eperson == null)
        {
            return AuthenticationMethod.NO_SUCH_USER;
        }
        else
        {
            // the person exists, just return ok
            context.setCurrentUser(eperson);
            request.getSession().setAttribute("shib.authenticated",
                    Boolean.TRUE);
        }

        return AuthenticationMethod.SUCCESS;
    }

    /**
     * Grab the special groups to be automatically provisioned for the current
     * user. Currently the mapping for the groups is done one-to-one, future
     * version can consider the usage of regex for such mapping.
     */
    public int[] getSpecialGroups(Context context, HttpServletRequest request)
    {
        // no user logged in or user not logged from shibboleth
        if (request == null || context.getCurrentUser() == null
                || request.getSession().getAttribute("shib.authenticated") == null)
        {
            return new int[0];
        }
                
        if (request.getSession().getAttribute("shib.specialgroup") != null)
        {
            return (int[]) request.getSession().getAttribute(
                    "shib.specialgroup");
        }

        java.util.Set groups = new java.util.HashSet();
        String roleHeader = ConfigurationManager
                .getProperty("authentication.shib.role-header");
        boolean roleHeader_ignoreScope = ConfigurationManager
                .getBooleanProperty("authentication.shib.role-header.ignore-scope");
        if (roleHeader == null || roleHeader.trim().length() == 0)
        {
            roleHeader = "Shib-EP-UnscopedAffiliation";
        } // fall back to default
        String affiliations = request.getHeader(roleHeader);

        // try again with all lower case...maybe has better luck
        if (affiliations == null)
        {
            affiliations = request.getHeader(roleHeader.toLowerCase());
        }

        // default role when fully authN but not releasing any roles?
        String defaultRoles = ConfigurationManager
                .getProperty("authentication.shib.default-roles");
        if (affiliations == null && defaultRoles != null)
        {
            affiliations = defaultRoles;
        }

        if (affiliations != null)
        {
            java.util.StringTokenizer st = new java.util.StringTokenizer(
                    affiliations, ";,");
            // do the mapping here
            while (st.hasMoreTokens())
            {
                String affiliation = st.nextToken().trim();

                // strip scope if present and roleHeader_ignoreScope
                if (roleHeader_ignoreScope) 
                {
                        int index = affiliation.indexOf('@');
                        if (index != -1)
                        {
                            affiliation = affiliation.substring(0, index);
                        }
                }

                // perform mapping here if necessary
                String groupLabels = ConfigurationManager
                        .getProperty("authentication.shib.role." + affiliation);
                if (groupLabels == null || groupLabels.trim().length() == 0)
                {
                    groupLabels = ConfigurationManager
                            .getProperty("authentication.shib.role."
                                    + affiliation.toLowerCase());
                }

                // revert back to original entry when no mapping is provided
                if (groupLabels == null)
                {
                    groupLabels = affiliation;
                }

                String[] labels = groupLabels.split(",");
                for (int i = 0; i < labels.length; i++)
                {
                    addGroup(groups, context, labels[i].trim());
                }
            }
        }

        int ids[] = new int[groups.size()];
        java.util.Iterator it = groups.iterator();
        for (int i = 0; it.hasNext(); i++)
        {
            ids[i] = ((Integer) it.next()).intValue();
        }

        // store the special group, if already transformed from headers
        // since subsequent header may not have the values anymore
        if (ids.length != 0)
        {
            request.getSession().setAttribute("shib.specialgroup", ids);
        }

        return ids;
    }

    /** Find dspaceGroup in DSpace database, if found, include it into groups */
    private void addGroup(Collection groups, Context context, String dspaceGroup)
    {
        try
        {
            Group g = Group.findByName(context, dspaceGroup);
            if (g == null)
            {
                // oops - no group defined
                log.warn(LogManager.getHeader(context, dspaceGroup
                        + " group is not found!! Admin needs to create one!",
                        "requiredGroup=" + dspaceGroup));
                groups.add(Integer.valueOf(0));
            }
            else
            {
                groups.add(Integer.valueOf(g.getID()));
            }
            log.info("Mapping group: " + dspaceGroup + " to groupID: "
                    + (g == null ? 0 : g.getID()));
        }
        catch (SQLException e)
        {
            log.error("Mapping group:" + dspaceGroup + " failed with error", e);
        }
    }

    /**
     * Indicate whether or not a particular self-registering user can set
     * themselves a password in the profile info form.
     * 
     * @param context
     *            DSpace context
     * @param request
     *            HTTP request, in case anything in that is used to decide
     * @param email
     *            e-mail address of user attempting to register
     * 
     */
    public boolean allowSetPassword(Context context,
            HttpServletRequest request, String email) throws SQLException
    {
        // don't use password at all
        return false;
    }

    /**
     * Predicate, is this an implicit authentication method. An implicit method
     * gets credentials from the environment (such as an HTTP request or even
     * Java system properties) rather than the explicit username and password.
     * For example, a method that reads the X.509 certificates in an HTTPS
     * request is implicit.
     * 
     * @return true if this method uses implicit authentication.
     */
    public boolean isImplicit()
    {
        return true;
    }

    /**
     * Indicate whether or not a particular user can self-register, based on
     * e-mail address.
     * 
     * @param context
     *            DSpace context
     * @param request
     *            HTTP request, in case anything in that is used to decide
     * @param username
     *            e-mail address of user attempting to register
     * 
     */
    public boolean canSelfRegister(Context context, HttpServletRequest request,
            String username) throws SQLException
    {
        return true;
    }

    /**
     * Initialise a new e-person record for a self-registered new user.
     * 
     * @param context
     *            DSpace context
     * @param request
     *            HTTP request, in case it's needed
     * @param eperson
     *            newly created EPerson record - email + information from the
     *            registration form will have been filled out.
     * 
     */
    public void initEPerson(Context context, HttpServletRequest request,
            EPerson eperson) throws SQLException
    {

    }

    /**
     * Get login page to which to redirect. Returns URL (as string) to which to
     * redirect to obtain credentials (either password prompt or e.g. HTTPS port
     * for client cert.); null means no redirect.
     * 
     * @param context
     *            DSpace context, will be modified (ePerson set) upon success.
     * 
     * @param request
     *            The HTTP request that started this operation, or null if not
     *            applicable.
     * 
     * @param response
     *            The HTTP response from the servlet method.
     * 
     * @return fully-qualified URL or null
     */
    public String loginPageURL(Context context, HttpServletRequest request,
            HttpServletResponse response)
    {
        return response.encodeRedirectURL(request.getContextPath()
                + "/shibboleth-login");
    }

    /**
     * Get title of login page to which to redirect. Returns a <i>message
     * key</i> that gets translated into the title or label for "login page" (or
     * null, if not implemented) This title may be used to identify the link to
     * the login page in a selection menu, when there are multiple ways to
     * login.
     * 
     * @param context
     *            DSpace context, will be modified (ePerson set) upon success.
     * 
     * @return title text.
     */
    public String loginPageTitle(Context context)
    {
        return "org.dspace.authenticate.ShibAuthentication.title";
    }
}
