/*
 *
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation. 
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt). 
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA 
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */

package com.sun.midp.security;

import javax.microedition.io.*;

import javax.microedition.lcdui.*;

import com.sun.midp.lcdui.*;

import com.sun.midp.midlet.*;

import com.sun.midp.i18n.Resource;
import com.sun.midp.i18n.ResourceConstants;

import com.sun.midp.events.EventQueue;

import com.sun.midp.io.j2me.storage.*;
import com.sun.midp.configurator.Constants;

import com.sun.midp.log.Logging;
import com.sun.midp.log.LogChannels;

/**
 * Contains methods to handle with the various security state information of a
 * a MIDlet suite.
 */
public final class SecurityHandler {
    /** Session level interaction has not occured. */
    private final static byte NOT_ASKED = 0;

    /** User granted permission for this session. */
    private final static byte GRANTED = 1;

    /** User denied permission for this session. */
    private final static byte DENIED = -1;

    /** The security token for this class. */
    private static SecurityToken classSecurityToken;

    /** The standard security exception message. */
    public static final String STD_EX_MSG = "Application not authorized " +
                                            "to access the restricted API";

    /** Permission list. */
    private byte permissions[];

    /** A flag for the session value of each permission. */
    private byte sessionValues[];

    /** Maximum permission level list. */
    private byte maxPermissionLevels[];

    /** True, if trusted. */
    private boolean trusted;

    /**
     * Creates a security domain with a list of permitted actions or no list
     * to indicate all actions. The caller must be have permission for
     * <code>Permissions.MIDP</code> or be the first caller of
     * the method for this instance of the VM.
     *
     * @param apiPermissions for the token
     * @param domain name of the security domain
     *
     * @exception SecurityException if caller is not permitted to call this
     *            method
     */
    public SecurityHandler(byte[] apiPermissions, String domain) {
        MIDletStateHandler midletStateHandler =
            MIDletStateHandler.getMidletStateHandler();
        MIDletSuite midletSuite = midletStateHandler.getMIDletSuite();

        midletSuite.checkIfPermissionAllowed(Permissions.AMS);
        init(apiPermissions, domain);
    }

    /**
     * Creates a security domain with a list of permitted actions or no list
     * to indicate all actions. The caller must be have permission for
     * <code>Permissions.MIDP</code> or be the first caller of
     * the method for this instance of the VM.
     *
     * @param securityToken security token of the caller
     * @param apiPermissions for the token, can be null
     * @param domain name of the security domain
     *
     * @exception SecurityException if caller is not permitted to call this
     *            method
     */
    public SecurityHandler(SecurityToken securityToken,
            byte[] apiPermissions, String domain) {
        securityToken.checkIfPermissionAllowed(Permissions.AMS);
        init(apiPermissions, domain);
    }

    /**
     * Creates a security domain with a list of permitted actions or no list
     * to indicate all actions. The caller must be have permission for
     * <code>Permissions.MIDP</code> or be the first caller of
     * the method for this instance of the VM.
     *
     * @param apiPermissions for the token
     * @param domain name of the security domain
     *
     * @exception SecurityException if caller is not permitted to call this
     *            method
     */
    private void init(byte[] apiPermissions, String domain) {
        MIDletStateHandler midletStateHandler =
            MIDletStateHandler.getMidletStateHandler();
        MIDletSuite midletSuite = midletStateHandler.getMIDletSuite();

        maxPermissionLevels =
            (Permissions.forDomain(domain))[Permissions.MAX_LEVELS];

        permissions = apiPermissions;

        sessionValues = new byte[permissions.length];

        trusted = Permissions.isTrusted(domain);
    }

    /**
     * Get the status of the specified permission.
     * If no API on the device defines the specific permission
     * requested then it must be reported as denied.
     * If the status of the permission is not known because it might
     * require a user interaction then it should be reported as unknown.
     *
     * @param permission to check if denied, allowed, or unknown.
     * @return 0 if the permission is denied; 1 if the permission is allowed;
     *  -1 if the status is unknown
     */
    public int checkPermission(String permission) {
        boolean found = false;
        int i;

        synchronized (this) {
            for (i = 0; i < Permissions.NUMBER_OF_PERMISSIONS; i++) {
                if (Permissions.getName(i).equals(permission)) {
                    found = true;
                    break;
                }
            }

            if (!found) {
                // report denied
                return 0;
            }

            switch (permissions[i]) {
            case Permissions.ALLOW:
            case Permissions.BLANKET_GRANTED:
                // report allowed
                return 1;

            case Permissions.SESSION:
                if (sessionValues[i] == GRANTED) {
                    // report allowed
                    return 1;
                }

                if (sessionValues[i] == DENIED) {
                    // report denied
                    return 0;
                }

                // fall through
            case Permissions.BLANKET:
            case Permissions.ONESHOT:
                // report unknown
                return -1;

            default:
                // Permissions.NEVER
                break;
            }

            // report denied
            return 0;
        }
    }

    /**
     * Check for permission and throw an exception if not allowed.
     * May block to ask the user a question.
     * <p>
     * The title, and question strings will be translated,
     * if a string resource is available.
     * Since the strings can have substitution token in them, if there is a
     * "%" it must changed to "%%". If a string has a %1, the app parameter
     * will be substituted for it. If a string has a "%2, the resource
     * parameter will be substituted for it. If a string has a %3, the
     * extraValue parameter will be substituted for it.
     *
     * @param permission ID of the permission to check for,
     *      the ID must be from
     *      {@link com.sun.midp.security.Permissions}
     * @param title Resource constant for the title of the dialog
     * @param question Resource constant for the question to ask the user
     * @param oneshotQuestion Resource constant for the oneshot question to
     *                        ask the user
     * @param app name of the application to insert into a string
     *        can be null if no %1 a string
     * @param resource string to insert into a string,
     *        can be null if no %2 in a string
     * @param extraValue string to insert into a string,
     *        can be null if no %3 in a string
     *
     * @return <code>true</code> if the permission interaction has permanently
     * changed and the new state should be saved, this will only happen
     * if the permission granted
     *
     * @exception SecurityException if the permission is not
     *            allowed by this token
     * @exception InterruptedException if another thread interrupts the
     *   calling thread while this method is waiting to preempt the
     *   display.
     */
    public boolean checkForPermission(int permission, int title, int question,
        int oneshotQuestion, String app, String resource, String extraValue)
        throws InterruptedException {

        return checkForPermission(permission, title, question,
            oneshotQuestion, app, resource, extraValue, STD_EX_MSG);
    }


    /**
     * Check for permission and throw an exception if not allowed.
     * May block to ask the user a question.
     * <p>
     * The title, question, and answer strings will be translated,
     * if a string resource is available.
     * Since the strings can have substitution token in them, if there is a
     * "%" it must changed to "%%". If a string has a %1, the app parameter
     * will be substituted for it. If a string has a "%2, the resource
     * parameter will be substituted for it. If a string has a %3, the
     * extraValue parameter will be substituted for it.
     *
     * @param permission ID of the permission to check for,
     *      the ID must be from
     *      {@link com.sun.midp.security.Permissions}
     * @param title Resource constant for the title of the dialog
     * @param question Resource constant for the question to ask user
     * @param oneShotQuestion Resource constant for the oneshot question to
     *                        ask the user
     * @param app name of the application to insert into a string
     *        can be null if no %1 a string
     * @param resource string to insert into a string,
     *        can be null if no %2 in a string
     * @param extraValue string to insert into a string,
     *        can be null if no %3 in a string
     * @param exceptionMsg message if a security exception is thrown
     *
     * @return <code>true</code> if the permission interaction has permanently
     * changed and the new state should be saved, this will only happen
     * if the permission granted
     *
     * @exception SecurityException if the permission is not
     *            allowed by this token
     * @exception InterruptedException if another thread interrupts the
     *   calling thread while this method is waiting to preempt the
     *   display.
     */
    public boolean checkForPermission(int permission, int title, int question,
        int oneShotQuestion, String app, String resource, String extraValue,
        String exceptionMsg) throws InterruptedException {

        if (permissions == null) {
            /* totally trusted, all permissions allowed */
            return false;
        }

        synchronized (this) {
            if (permission >= 0 && permission < permissions.length) {
                switch (permissions[permission]) {
                case Permissions.ALLOW:
                case Permissions.BLANKET_GRANTED:
                    return false;

                case Permissions.BLANKET:
                    /* This level means the question has not been asked yet. */
                    if (askUserForPermission(classSecurityToken, trusted,
                            title, question, app, resource, extraValue)) {

                        Permissions.setPermissionGroup(permissions,
                            permission, Permissions.BLANKET_GRANTED);

                        return true;
                    }

                    Permissions.setPermissionGroup(permissions,
                        permission, Permissions.BLANKET_DENIED);
                    break;

                case Permissions.SESSION:
                    if (sessionValues[permission] == GRANTED) {
                        return false;
                    }

                    if (sessionValues[permission] == DENIED) {
                        break;
                    }

                    if (askUserForPermission(classSecurityToken, trusted,
                            title, question, app, resource, extraValue)) {
                        /*
                         * Save the fact that the question has already
                         * been asked this session.
                         */
                        Permissions.setPermissionGroup(sessionValues,
                            permission, GRANTED);

                        return false;
                    }

                    /*
                     * Save the fact that the question has already
                     * been asked this session.
                     */
                    Permissions.setPermissionGroup(sessionValues,
                        permission, DENIED);
                    break;

                case Permissions.ONESHOT:
                    if (askUserForPermission(classSecurityToken, trusted,
                            title, oneShotQuestion, app, resource,
                            extraValue)) {
                        return false;
                    }

                    break;

                default:
                    // Permissions.NEVER
                    break;
                } // switch
            } // if

            throw new SecurityException(exceptionMsg);
        } // synchronized
    }

    /**
     * Ask the user yes/no permission question.
     *
     * @param token security token with the permission to preempt the
     *        foreground display
     * @param trusted true to display the trusted icon, false to display the
     *                untrusted icon
     * @param title Resource constant for the title of the dialog
     * @param question Resource constant for the question to ask user
     * @param app name of the application to insert into a string
     *        can be null if no %1 a string
     * @param resource string to insert into a string,
     *        can be null if no %2 in a string
     * @param extraValue string to insert into a string,
     *        can be null if no %3 in a string
     *
     * @return true if the user says yes else false
     *
     * @exception InterruptedException if another thread interrupts the
     *   calling thread while this method is waiting to preempt the
     *   display.
     */
    public static boolean askUserForPermission(SecurityToken token,
            boolean trusted, int title, int question, String app,
            String resource, String extraValue) throws InterruptedException {

        PermissionDialog dialog =
            new PermissionDialog(token, trusted, title, question, app,
                                 resource, extraValue);

        return dialog.waitForAnswer();
    }

    /**
     * Initializes the security token for this class, so it can
     * perform actions that a normal MIDlet Suite cannot.
     *
     * @param token security token for this class.
     */
    static void initSecurityToken(SecurityToken token) {
        if (classSecurityToken != null) {
            return;
        }

        classSecurityToken = token;
    }
}

/** Implements security permission dialog. */
class PermissionDialog implements CommandListener {
    /** Caches the display manager reference. */
    private DisplayEventHandler displayEventHandler;

    /** Permission Alert. */
    private Alert alert;

    /** Command object for "Yes" command. */
    private Command yesCmd =
        new Command(Resource.getString(ResourceConstants.YES),
                    Command.OK, 1);
    /** Command object for "No" command. */
    private Command noCmd =
        new Command(Resource.getString(ResourceConstants.NO),
                    Command.BACK, 1);
    /** Holds the preempt token so the form can end. */
    private Object preemptToken;

    /** Holds the answer to the security question. */
    private boolean answer;

    /**
     * Construct permission dialog.
     * <p>
     * The title, question, and answer strings will be translated,
     * if a string resource is available.
     * Since the strings can have substitution token in them, if there is a
     * "%" it must changed to "%%". If a string has a %1, the app parameter
     * will be substituted for it. If a string has a "%2, the resource
     * parameter will be substituted for it. If a string has a %3, the
     * extraValue parameter will be substituted for it.
     *
     * @param token security token with the permission to preempt the
     *        foreground display
     * @param trusted true to display the trusted icon, false to display the
     *                untrusted icon
     * @param title Resource constant for the title of the dialog
     * @param question Resource constant for the question to ask user
     * @param app name of the application to insert into a string
     *        can be null if no %1 a string
     * @param resource string to insert into a string,
     *        can be null if no %2 in a string
     * @param extraValue string to insert into a string,
     *        can be null if no %3 in a string
     *
     * @exception InterruptedException if another thread interrupts the
     *   calling thread while this method is waiting to preempt the
     *   display.
     */
    PermissionDialog(SecurityToken token, boolean trusted, int title,
            int question, String app, String resource, String extraValue)
            throws InterruptedException {
        String[] substitutions = {app, resource, extraValue};
        String iconFilename;
        RandomAccessStream stream;
        byte[] rawPng;
        Image icon;
        String configRoot = File.getConfigRoot(Constants.INTERNAL_STORAGE_ID);

        alert = new Alert(Resource.getString(title, substitutions));

        displayEventHandler =
            DisplayEventHandlerFactory.getDisplayEventHandler(token);

        if (trusted) {
            iconFilename = configRoot + "trusted_icon.png";
        } else {
            iconFilename = configRoot + "untrusted_icon.png";
        }

        stream = new RandomAccessStream(token);
        try {
            stream.connect(iconFilename, Connector.READ);
            rawPng = new byte[stream.getSizeOf()];
            stream.readBytes(rawPng, 0, rawPng.length);
            stream.disconnect();
            icon = Image.createImage(rawPng, 0, rawPng.length);
            alert.setImage(icon);
        } catch (java.io.IOException noImage) {
        }

        alert.setString(Resource.getString(question, substitutions));
        alert.addCommand(noCmd);
        alert.addCommand(yesCmd);
        alert.setCommandListener(this);
        preemptToken = displayEventHandler.preemptDisplay(alert, true);
    }

    /**
     * Waits for the user's answer.
     *
     * @return user's answer
     */
    boolean waitForAnswer() {
        synchronized (this) {
            if (preemptToken == null) {
                return false;
            }

            if (EventQueue.isDispatchThread()) {
                // Developer programming error
                throw new RuntimeException(
                "Blocking call performed in the event thread");
            }

            try {
                wait();
            } catch (Throwable t) {
                return false;
            }

            return answer;
        }
    }

    /**
     * Sets the user's answer and notifies waitForAnswer and
     * ends the form.
     *
     * @param theAnswer user's answer
     */
    private void setAnswer(boolean theAnswer) {
        synchronized (this) {
            answer = theAnswer;

            /*
             * Since this may be the only display, clear the alert,
             * so the user will not be confused by alert text still
             * displaying.
             *
             * The case should happen when running TCK test MIDlets in
             * SVM mode.
             */
            alert.setTitle(null);
            alert.setString(null);
            alert.setImage(null);
            alert.addCommand(new Command("", 1, 1));
            alert.removeCommand(noCmd);
            alert.removeCommand(yesCmd);

            displayEventHandler.donePreempting(preemptToken);

            notify();
        }

    }

    /**
     * Respond to a command issued on security question form.
     *
     * @param c command activated by the user
     * @param s the Displayable the command was on.
     */
    public void commandAction(Command c, Displayable s) {
        if (c == yesCmd) {
            setAnswer(true);
            return;
        }

        setAnswer(false);
    }
}
