/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const Ci = Components.interfaces;
const Cu = Components.utils;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AppsUtils.jsm");
Cu.import("resource://gre/modules/PermissionsInstaller.jsm");
Cu.import("resource://gre/modules/PermissionsTable.jsm");
Cu.import("resource://gre/modules/PermissionSettings.jsm");

XPCOMUtils.defineLazyServiceGetter(this, "dataStoreService",
                                   "@mozilla.org/datastore-service;1",
                                   "nsIDataStoreService");

this.EXPORTED_SYMBOLS = ["SystemMessagePermissionsChecker",
                         "SystemMessagePermissionsTable"];

function debug(aStr) {
  // dump("SystemMessagePermissionsChecker.jsm: " + aStr + "\n");
}

// This table maps system message to permission(s), indicating only
// the system messages granted by the page's permissions are allowed
// to be registered or sent to that page. Note the empty permission
// set means this type of system message is always permitted.

this.SystemMessagePermissionsTable = {
  "activity": { },
  "alarm": {
    "alarms": []
  },
  "bluetooth-dialer-command": {
    "telephony": []
  },
  "bluetooth-cancel": {
    "bluetooth": []
  },
  "bluetooth-hid-status-changed": {
    "bluetooth": []
  },
  "bluetooth-pairing-request": {
    "bluetooth": []
  },
  "bluetooth-opp-transfer-complete": {
    "bluetooth": []
  },
  "bluetooth-opp-update-progress": {
    "bluetooth": []
  },
  "bluetooth-opp-receiving-file-confirmation": {
    "bluetooth": []
  },
  "bluetooth-opp-transfer-start": {
    "bluetooth": []
  },
  "cellbroadcast-received": {
    "cellbroadcast": []
  },
  "connection": { },
  "captive-portal": {
    "wifi-manage": []
  },
  "dummy-system-message": { }, // for system message testing framework
  "headset-button": { },
  "icc-stkcommand": {
    "settings": ["read", "write"]
  },
  "media-button": { },
  "networkstats-alarm": {
    "networkstats-manage": []
  },
  "notification": {
    "desktop-notification": []
  },
  "push": {
  	"push": []
  },
  "push-register": {
  	"push": []
  },
  "request-sync": { },
  "sms-delivery-success": {
    "sms": []
  },
  "sms-read-success": {
    "sms": []
  },
  "sms-received": {
    "sms": []
  },
  "sms-sent": {
    "sms": []
  },
  "telephony-new-call": {
    "telephony": []
  },
  "telephony-call-ended": {
    "telephony": []
  },
  "ussd-received": {
    "mobileconnection": []
  },
  "wappush-received": {
    "wappush": []
  },
  "cdma-info-rec-received": {
    "mobileconnection": []
  },
  "nfc-hci-event-transaction": {
    "nfc-hci-events": []
  },
  "nfc-manager-tech-discovered": {
    "nfc-manager": []
  },
  "nfc-manager-tech-lost": {
    "nfc-manager": []
  },
  "nfc-manager-send-file": {
    "nfc-manager": []
  },
  "wifip2p-pairing-request": { },
  "first-run-with-sim": {
    "settings": ["read", "write"]
  }
};


this.SystemMessagePermissionsChecker = {
  /**
   * Return all the needed permission names for the given system message.
   * @param string aSysMsgName
   *        The system messsage name.
   * @returns object
   *        Format: { permName (string): permNamesWithAccess (string array), ... }
   *        Ex, { "settings": ["settings-read", "settings-write"], ... }.
   *        Note: an empty object will be returned if it's always permitted.
   * @returns null
   *        Return and report error when any unexpected error is ecountered.
   *        Ex, when the system message we want to search is not included.
   **/
  getSystemMessagePermissions: function getSystemMessagePermissions(aSysMsgName) {
    debug("getSystemMessagePermissions(): aSysMsgName: " + aSysMsgName);

    let permNames = SystemMessagePermissionsTable[aSysMsgName];
    if (permNames === undefined) {
      debug("'" + aSysMsgName + "' is not associated with permissions. " +
            "Please add them to the SystemMessage[Prefix]PermissionsTable.");
      return null;
    }

    let object = { };
    for (let permName in permNames) {
      if (PermissionsTable[permName] === undefined) {
        debug("'" + permName + "' for '" + aSysMsgName + "' is invalid. " +
              "Please correct it in the SystemMessage[Prefix]PermissionsTable.");
        return null;
      }

      // Construct a new permission name array by adding the access suffixes.
      let access = permNames[permName];
      if (!access || !Array.isArray(access)) {
        debug("'" + permName + "' is not associated with access array. " +
              "Please correct it in the SystemMessage[Prefix]PermissionsTable.");
        return null;
      }
      object[permName] = appendAccessToPermName(permName, access);
    }
    return object
  },

  /**
   * Check if the message is a datastore-update message
   * @param string aSysMsgName
   *        The system messsage name.
   */
  isDataStoreSystemMessage: function(aSysMsgName) {
    return aSysMsgName.indexOf('datastore-update-') === 0;
  },

  /**
   * Check if this manifest can deliver this particular datastore message.
   */
  canDeliverDataStoreSystemMessage: function(aSysMsgName, aManifestURL) {
    let store = aSysMsgName.substr('datastore-update-'.length);

    // Get all the manifest URLs of the apps which can access the datastore.
    let manifestURLs = dataStoreService.getAppManifestURLsForDataStore(store);
    let enumerate = manifestURLs.enumerate();
    while (enumerate.hasMoreElements()) {
      let manifestURL = enumerate.getNext().QueryInterface(Ci.nsISupportsString);
      if (manifestURL == aManifestURL) {
        return true;
      }
    }

    return false;
  },

  /**
   * Check if the system message is permitted to be registered for the given
   * app at start-up based on the permissions claimed in the app's manifest.
   * @param string aSysMsgName
   *        The system messsage name.
   * @param string aManifestURL
   *        The app's manifest URL.
   * @param object aManifest
   *        The app's manifest.
   * @returns bool
   *        Is permitted or not.
   **/
  isSystemMessagePermittedToRegister:
    function isSystemMessagePermittedToRegister(aSysMsgName,
                                                aManifestURL,
                                                aManifest) {
    debug("isSystemMessagePermittedToRegister(): " +
          "aSysMsgName: " + aSysMsgName + ", " +
          "aManifestURL: " + aManifestURL + ", " +
          "aManifest: " + JSON.stringify(aManifest));

    if (this.isDataStoreSystemMessage(aSysMsgName) &&
        this.canDeliverDataStoreSystemMessage(aSysMsgName, aManifestURL)) {
      return true;
    }

    let permNames = this.getSystemMessagePermissions(aSysMsgName);
    if (permNames === null) {
      return false;
    }

    // Check to see if the 'webapp' is app/privileged/certified.
    let appStatus;
    switch (AppsUtils.getAppManifestStatus(aManifest)) {
    case Ci.nsIPrincipal.APP_STATUS_CERTIFIED:
      appStatus = "certified";
      break;
    case Ci.nsIPrincipal.APP_STATUS_PRIVILEGED:
      appStatus = "privileged";
      break;
    case Ci.nsIPrincipal.APP_STATUS_INSTALLED:
      appStatus = "app";
      if (aManifest.type == "trusted") {
        appStatus = "trusted";
      }
      break;
    default:
      throw new Error("SystemMessagePermissionsChecker.jsm: " +
                      "Cannot decide the app's status. Install cancelled.");
      break;
    }

    // It's ok here to not pass the origin to ManifestHelper since we only
    // need the permission property and that doesn't depend on uri resolution.
    let newManifest = new ManifestHelper(aManifest, aManifestURL, aManifestURL);

    for (let permName in permNames) {
      // The app doesn't claim valid permissions for this sytem message.
      if (!newManifest.permissions || !newManifest.permissions[permName]) {
        debug("'" + aSysMsgName + "' isn't permitted by '" + permName + "'. " +
              "Please add the permission for app: '" + aManifestURL + "'.");
        return false;
      }
      let permValue = PermissionsTable[permName][appStatus];
      if (permValue != Ci.nsIPermissionManager.PROMPT_ACTION &&
          permValue != Ci.nsIPermissionManager.ALLOW_ACTION) {
        debug("'" + aSysMsgName + "' isn't permitted by '" + permName + "'. " +
              "Please add the permission for app: '" + aManifestURL + "'.");
        return false;
      }

      // Compare the expanded permission names between the ones in
      // app's manifest and the ones needed for system message.
      let expandedPermNames =
        expandPermissions(permName,
                          newManifest.permissions[permName].access);

      let permNamesWithAccess = permNames[permName];

      // Early return false as soon as any permission is not matched.
      for (let idx in permNamesWithAccess) {
        let index = expandedPermNames.indexOf(permNamesWithAccess[idx]);
        if (index == -1) {
          debug("'" + aSysMsgName + "' isn't permitted by '" + permName + "'. " +
                "Please add the permission for app: '" + aOrigin + "'.");
          return false;
        }
      }
    }

    // All the permissions needed for this system message are matched.
    return true;
  },

  /**
   * Check if the system message is permitted to be sent to the given
   * app's page at run-time based on the current app's permissions.
   * @param string aSysMsgName
   *        The system messsage name.
   * @param string aPageURL
   *        The app's page URL.
   * @param string aManifestURL
   *        The app's manifest URL.
   * @returns bool
   *        Is permitted or not.
   **/
  isSystemMessagePermittedToSend:
    function isSystemMessagePermittedToSend(aSysMsgName, aPageURL, aManifestURL) {
    debug("isSystemMessagePermittedToSend(): " +
          "aSysMsgName: " + aSysMsgName + ", " +
          "aPageURL: " + aPageURL + ", " +
          "aManifestURL: " + aManifestURL);

    if (this.isDataStoreSystemMessage(aSysMsgName) &&
        this.canDeliverDataStoreSystemMessage(aSysMsgName, aManifestURL)) {
      return true;
    }

    let permNames = this.getSystemMessagePermissions(aSysMsgName);
    if (permNames === null) {
      return false;
    }

    let pageURI = Services.io.newURI(aPageURL, null, null);
    for (let permName in permNames) {
      let permNamesWithAccess = permNames[permName];

      // Early return false as soon as any permission is not matched.
      for (let idx in permNamesWithAccess) {
        if(PermissionSettingsModule.getPermission(permNamesWithAccess[idx],
                                                  aManifestURL,
                                                  pageURI.prePath,
                                                  false) != "allow") {
          debug("'" + aSysMsgName + "' isn't permitted by '" + permName + "'. " +
                "Please add the permission for app: '" + pageURI.prePath + "'.");
          return false;
        }
      }
    }

    // All the permissions needed for this system message are matched.
    return true;
  }
};
