/*
 * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the
 * License at http://www.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
package com.arsdigita.kernel.security;

import com.arsdigita.util.URLRewriter;
import com.arsdigita.kernel.Kernel;
import com.arsdigita.kernel.SiteNode;
import com.arsdigita.initializer.Configuration;
import com.arsdigita.initializer.InitializationException;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.security.auth.login.LoginException;
import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;

/**
 * <p>Initializes security properties.</p>
 *
 * <p><b><font color="red">Deprecated feature: </font></b> Provides access
 * to URLs for standard pages.</p>
 *
 * @author Sameer Ajmani
 * @since ACS 4.5
 **/
public class Initializer
    implements com.arsdigita.initializer.Initializer {

    public static final String versionId = "$Id: //core-platform/dev/src/com/arsdigita/kernel/security/Initializer.java#20 $ by $Author: dennis $, $DateTime: 2004/04/07 16:07:11 $";

    private static final Logger s_log =
        Logger.getLogger(Initializer.class);

    /** Obsolete parameter name for session tracking method. **/
    public static String SESSION_TRACKING_PARAM = "sessionTrackingMethod";
    /** Parameter name for login configuration. **/
    public static String LOGIN_CONFIG_PARAM = "loginConfig";
    /** Parameter name for security helper class name. **/
    public static String SECURITY_HELPER_PARAM = "securityHelperClass";
    /** Parameter name for excluded URI extensions. **/
    public static String EXCLUDED_EXTENSIONS_PARAM = "excludedExtensions";
    /** Size of secret key in bytes. **/
    public static int SECRET_KEY_BYTES = 16;

    /** When this is on new users get automatically redirected to the
     * create new user form */
    public static String AUTO_REGISTRATION_ON_PARAM ="autoRegistrationOn";
    private static boolean s_autoRegistrationOn;

    private Configuration m_conf = new Configuration();

    public Configuration getConfiguration() {
        return m_conf;
    }

    public Initializer() throws InitializationException {
        m_conf.initParameter
            (SECURITY_HELPER_PARAM,
             "The class name of the SecurityHelper implementation",
             String.class,
             DefaultSecurityHelper.class.getName());
        m_conf.initParameter
            (SESSION_TRACKING_PARAM,
             "This parameter is obsolete.",
             String.class);
        m_conf.initParameter
            (LOGIN_CONFIG_PARAM,
             "The login configuration.",
             List.class);
        m_conf.initParameter
            (EXCLUDED_EXTENSIONS_PARAM,
             "List of extensions excluded from authentication cookies. "
             +"Authentication is checked for all requests, but requests "
             +"with one of these extensions will never cause a new cookie "
             +"to be set.  Include a leading dot for each extension.",
             List.class,
             Arrays.asList(new String[] { ".jpg", ".gif", ".png", ".pdf" }));


        // NOTE: default page map is loaded explicitly in loadPageMap
        // so that users can safely override a subset of the entries

        m_conf.initParameter
            (AUTO_REGISTRATION_ON_PARAM,
             "When this parameter is set to true, new users get automatically" 
             + "redirected to the create new user form",
             Boolean.class,
             Boolean.TRUE);
    }

    private void loadAutoRegistrationOn() {
        s_autoRegistrationOn = ((Boolean)m_conf.getParameter(
                                    AUTO_REGISTRATION_ON_PARAM)).booleanValue();
    }

    public static boolean getAutoRegistrationOn() {
        return s_autoRegistrationOn;
    }

    public void startup() throws InitializationException {
        URLRewriter.addParameterProvider
            (new SecurityParameterProvider());
        loadExcludedExtensions();
        loadSecurityHelper();
        loadPageMap();
        loadLoginConfig();
        loadAutoRegistrationOn();
    }

    /**
     * Returns an iterator over the list of excluded extensions.
     *
     * @return an iterator over the list of excluded extensions.
     *
     * @deprecated To be moved into a utility class.
     **/
    public static Iterator getExcludedExtensions() {
        if (s_exts == null) {
            return java.util.Collections.EMPTY_LIST.iterator();
        } else {
            return s_exts.iterator();
        }
    }
    private static List s_exts = null;

    private void loadExcludedExtensions() {
        s_exts = (List)m_conf.getParameter(EXCLUDED_EXTENSIONS_PARAM);
        Iterator exts = getExcludedExtensions();
        while (exts.hasNext()) {
            Object o = exts.next();
            if (!(o instanceof String)) {
                throw new InitializationException
                    ("Extension must be a string: "+o);
            }
        }
    }

    /**
     * Returns the security helper instance.
     *
     * @return the security helper instance.
     *
     * @deprecated Moved into {@link Util}
     **/
    public static SecurityHelper getSecurityHelper() {
        return Util.getSecurityHelper();
    }

    private void loadSecurityHelper() {
        String name = (String)m_conf.getParameter(SECURITY_HELPER_PARAM);
        if (name == null) {
            throw new InitializationException
                (SECURITY_HELPER_PARAM+" not defined");
        }
        try {
            Class theClass = Class.forName(name);
            if (!SecurityHelper.class.isAssignableFrom(theClass)) {
                throw new InitializationException
                    (SECURITY_HELPER_PARAM+": "+name
                     +" does not implement interface "
                     +SecurityHelper.class.getName());
            }
            Util.setSecurityHelper(theClass.newInstance());
        } catch (ClassNotFoundException e) {
            throw new InitializationException
                (SECURITY_HELPER_PARAM+": "+name+" not found: ", e);
        } catch (InstantiationException e) {
            throw new InitializationException
                (SECURITY_HELPER_PARAM+": "+name
                 +" is not concrete or lacks no-arg constructor: ", e);
        } catch (IllegalAccessException e) {
            throw new InitializationException
                (SECURITY_HELPER_PARAM+": "+name
                 +" is not public or lacks public constructor: ", e);
        }
    }

    /** Key for the root page of the site. **/
    public static String ROOT_PAGE_KEY =
        "com.arsdigita.page.kernel.root";
    /** Key for the user edit page. **/
    public static String EDIT_PAGE_KEY =
        "com.arsdigita.page.kernel.edit";
    /** Key for the login page. **/
    public static String LOGIN_PAGE_KEY =
        "com.arsdigita.page.kernel.login";
    /** Key for the new user page. **/
    public static String NEWUSER_PAGE_KEY =
        "com.arsdigita.page.kernel.newuser";
    /** Key for the logout page. **/
    public static String LOGOUT_PAGE_KEY =
        "com.arsdigita.page.kernel.logout";
    /** Key for the explain-cookies page. **/
    public static String COOKIES_PAGE_KEY =
        "com.arsdigita.page.kernel.cookies";
    /** Key for the login-expired page. **/
    public static String EXPIRED_PAGE_KEY =
        "com.arsdigita.page.kernel.expired";
    /** Key for the change-password page. **/
    public static String CHANGE_PAGE_KEY =
        "com.arsdigita.page.kernel.change";
    /** Key for the recover-password page. **/
    public static String RECOVER_PAGE_KEY =
        "com.arsdigita.page.kernel.recover";
    /** Key for the workspace page. **/
    public static String WORKSPACE_PAGE_KEY =
        "com.arsdigita.page.kernel.workspace";
    /** Key for the login redirect url. **/
    public static String LOGIN_REDIRECT_PAGE_KEY =
        "com.arsdigita.page.kernel.login.redirect";
    /** Key for the admin-permission page. **/
    public static String PERMISSION_PAGE_KEY =
        "com.arsdigita.page.kernel.permission";
    /** Key for the single-permission page. **/
    public static String PERM_SINGLE_PAGE_KEY =
        "com.arsdigita.page.kernel.perm-single";

    private static List s_defaultPageMap = new ArrayList() {
            {
                put(ROOT_PAGE_KEY, "register/");
                put(EDIT_PAGE_KEY, "register/edit-profile");
                put(LOGIN_PAGE_KEY, "register/");
                put(NEWUSER_PAGE_KEY, "register/new-user");
                put(LOGOUT_PAGE_KEY, "register/logout");
                put(COOKIES_PAGE_KEY, "register/explain-persistent-cookies");
                put(CHANGE_PAGE_KEY, "register/change-password");
                put(RECOVER_PAGE_KEY, "register/recover-password");
                put(EXPIRED_PAGE_KEY, "register/login-expired");
                put(WORKSPACE_PAGE_KEY, "pvt/");
                put(LOGIN_REDIRECT_PAGE_KEY, "pvt/");
                put(PERMISSION_PAGE_KEY, "permissions/");
                put(PERM_SINGLE_PAGE_KEY, "permissions/one");
            }
            private void put(String key, String value) {
                add(Arrays.asList(new Object[] { key, value }));
            }
        };

    private static Map s_pageMap = new HashMap();



    private void loadPageMap() throws InitializationException {
        // load default page map
        loadPageMap(s_defaultPageMap);
        // load user page map
        
        List list = new ArrayList() {
                {
                    SecurityConfig conf = Kernel.getSecurityConfig();
                    put(ROOT_PAGE_KEY, conf.getRootPage());
                    put(LOGIN_PAGE_KEY, conf.getLoginPage());
                    put(NEWUSER_PAGE_KEY, conf.getNewUserPage());
                    put(LOGOUT_PAGE_KEY, conf.getLogoutPage());
                    put(COOKIES_PAGE_KEY, conf.getCookiesPage());
                    put(CHANGE_PAGE_KEY, conf.getChangePage());
                    put(RECOVER_PAGE_KEY, conf.getRecoverPage());
                    put(EXPIRED_PAGE_KEY, conf.getExpiredPage());
                    put(WORKSPACE_PAGE_KEY, conf.getWorkspacePage());
                    put(LOGIN_REDIRECT_PAGE_KEY, conf.getLoginRedirectPage());
                    put(PERMISSION_PAGE_KEY, conf.getPermissionPage());
                    put(PERM_SINGLE_PAGE_KEY, conf.getPermSinglePage());
                }
                private void put(String key, String value) {
                    add(Arrays.asList(new Object[] { key, value }));
                }
            };
        if (list != null) {
            s_log.info("Security Initializer: mapping "
                       +list.size()+" pages");
            loadPageMap(list);
        }
    }

    private void loadPageMap(List list) {
        Iterator pairs = list.iterator();
        while (pairs.hasNext()) {
            List pair = (List)pairs.next();
            String key = (String)pair.get(0);
            String url = (String)pair.get(1);
            s_pageMap.put(key, url);
        }
    }

    /**
     * Returns the relative URL associated with the given key.  This is the
     * value of the URL in the page map for the given key.
     *
     * @return the relative URL associated with the given key, or null if it
     * does not exist.
     *
     * @deprecated To be replaced by package parameters.
     *
     * @see #getFullURL(String, HttpServletRequest)
     **/
    public static String getURL(String key) {
        return (String)s_pageMap.get(key);
    }

    /**
     * Returns the absolute URL associated with the given key.  This is the
     * root URL for the system (the mount point) prepended to the result of
     * getURL(key).
     *
     * @return the absolute URL associated with the given key, or null
     * if it does not exist.
     *
     * @see #getURL(String)
     **/
    public static String getFullURL(String key, HttpServletRequest req) {
        String root = getRootURL(req);
        String url = getURL(key);
        
        if (s_log.isDebugEnabled()) {
            s_log.debug("Root is " + root + ", url is " + url);
        }

        if ((root == null) || (key == null)) {
            return null;
        }
        return root + url;
    }

    private static String getRootURL(HttpServletRequest req) {
        // XXX this isn't safe since you aren't neccessarily
        // calling it from the root webapp - so we can't
        // blindly prepend the context path from the current
        // request.
        //return SiteNode.getRootSiteNode().getURL(req);
        
        return SiteNode.getRootSiteNode().getURL();
    }

    private void loadLoginConfig() throws InitializationException {
        checkLibrary();

        javax.security.auth.login.Configuration.setConfiguration
            (getLoginConfig());

        checkLoginConfig();
    }

    private void checkLibrary() throws InitializationException {
        // check that JAAS is available
        // TODO: this check should not be needed in JDK 1.4, since JAAS will
        // be integrated into the JDK.  Also, JAAS classes will no longer be
        // required to be loaded by the system classloader as in JDK 1.3.
        // We should also be able to use JAAS's LoginContext instead of our
        // own version of LoginContext.
        try {
            ClassLoader.getSystemClassLoader().loadClass
                ("javax.security.auth.Subject");
            ClassLoader.getSystemClassLoader().loadClass
                ("javax.security.auth.login.Configuration");
            ClassLoader.getSystemClassLoader().loadClass
                ("com.sun.security.auth.login.ConfigFile");
            s_log.info("Security Initializer: JAAS lib found");
        } catch (ClassNotFoundException e) {
            reportMissingLibrary
                ("Java Authentication and Authorization Service (JAAS)",
                 "jaas.jar");
        }
    }

    private javax.security.auth.login.Configuration getLoginConfig()
        throws InitializationException {
        List list = (List)m_conf.getParameter(LOGIN_CONFIG_PARAM);
        if (list == null) {
            throw new InitializationException
                (LOGIN_CONFIG_PARAM+" not defined "
                 // TODO: remove the following comment eventually
                 +"(update enterprise.init to get the default login config; "
                 +"loginConfig replaces loginConfigFileName and eliminates "
                 +"the need to use a separate file for this information)");
        }
        return new LoginConfig(list);
    }

    private void checkLoginConfig() throws InitializationException {
        // check the login configurations
        String[] contexts = new String[] {
            UserContext.REQUEST_LOGIN_CONTEXT,
            UserContext.REGISTER_LOGIN_CONTEXT
        };
        for (int i = 0; i < contexts.length; i++) {
            try {
                new LoginContext(contexts[i]);
            } catch (LoginException e) {
                throw new InitializationException
                    ("Could not instantiate login context '"
                     +contexts[i]+"'.  "
                     +"Check that it is defined in your login "
                     +"configuration.", e);
            }
        }
    }

    /**
     * Report a missing library during initialization.  Package-visible.
     *
     * @throws InitializationException with a detail message describing how
     * to install the named library.
     **/
    static void reportMissingLibrary(String libraryName,
                                     String libraryJar)
        throws InitializationException {
        throw new InitializationException
            ("Could not find the "+libraryName+".  "
             +"This library ("+libraryJar+") "
             +"must be on your CLASSPATH or "
             +"in your extensions directory "
             +"($JAVA_HOME/jre/lib/ext/).");
    }

    public void shutdown() throws InitializationException {
        // do nothing
    }
}
