/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * 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.
 *
 * The Original Code is the Update Service.
 *
 * The Initial Developer of the Original Code is Ben Goodger.
 * Portions created by the Initial Developer are Copyright (C) 2004
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *  Ben Goodger <ben@mozilla.org> (Original Author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

const PREF_APP_UPDATE_ENABLED             = "app.update.enabled";
const PREF_APP_UPDATE_AUTOINSTALL_ENABLED = "app.update.autoInstallEnabled";
// Defines the notification behavior when updates are encountered
//
// 0 = download and install all updates (including major versions, provided
//     no addons are made incompatible)
// 1 = download and install minor (security) updates
// 2 = ask before doing anything
//
const PREF_APP_UPDATE_AUTOINSTALL_MODE    = "app.update.autoInstallMode";
const PREF_APP_UPDATE_INTERVAL            = "app.update.interval";
const PREF_APP_UPDATE_TIMER               = "app.update.timer";

const PREF_APP_UPDATE_URL                 = "app.update.url";

const PREF_UPDATE_LASTUPDATETIME_FMT      = "app.update.lastUpdateTime.%ID%";
const PREF_APP_EXTENSIONS_VERSION         = "app.extensions.version";

const URI_UPDATE_PROMPT_DIALOG  = "chrome://mozapps/content/update/updates.xul";

const KEY_APPDIR          = "XCurProcD";

const DIR_UPDATES         = "updates";
const FILE_UPDATE_STATUS  = "update.status";
const FILE_UPDATE_ARCHIVE = "update.mar";
const FILE_UPDATE_INFO    = "update.info";

const MODE_RDONLY   = 0x01;
const MODE_WRONLY   = 0x02;
const MODE_CREATE   = 0x08;
const MODE_APPEND   = 0x10;
const MODE_TRUNCATE = 0x20;

const PERMS_FILE      = 0644;
const PERMS_DIRECTORY = 0755;

const STATE_DOWNLOADING = "downloading";
const STATE_PENDING     = "pending";
const STATE_APPLYING    = "applying";
const STATE_SUCCEEDED   = "succeeded";
const STATE_FAILED      = "failed";

const DOWNLOAD_CHUNK_SIZE           = 65536;
const DOWNLOAD_BACKGROUND_INTERVAL  = 1;
const DOWNLOAD_FOREGROUND_INTERVAL  = 0;

const nsILocalFile            = Components.interfaces.nsILocalFile;
const nsIUpdateService        = Components.interfaces.nsIUpdateService;
const nsIUpdateItem           = Components.interfaces.nsIUpdateItem;
const nsIPrefLocalizedString  = Components.interfaces.nsIPrefLocalizedString;
const nsIIncrementalDownload  = Components.interfaces.nsIIncrementalDownload;
const nsIFileInputStream      = Components.interfaces.nsIFileInputStream;
const nsIFileOutputStream     = Components.interfaces.nsIFileOutputStream;
const nsICryptoHash           = Components.interfaces.nsICryptoHash;

const Node = Components.interfaces.nsIDOMNode;

var gApp      = null;
var gPref     = null;
var gOS       = null;
var gConsole  = null;

/**
 * Logs a string to the error console. 
 * @param   string
 *          The string to write to the error console..
 */  
function LOG(string) {
  // dump("*** " + string + "\n");
  gConsole.logStringMessage(string);
}

/**
 * Convert a string containing binary values to hex.
 */
function binaryToHex(input) {
  var result = "";
  for (var i = 0; i < input.length; ++i) {
    result += input.charCodeAt(i).toString(16);
  }
  return result;
}

/**
 * Gets a File URL spec for a nsIFile
 * @param   file
 *          The file to get a file URL spec to
 * @returns The file URL spec to the file
 */
function getURLSpecFromFile(file) {
  var ioServ = Components.classes["@mozilla.org/network/io-service;1"]
                         .getService(Components.interfaces.nsIIOService);
  var fph = ioServ.getProtocolHandler("file")
                  .QueryInterface(Components.interfaces.nsIFileProtocolHandler);
  return fph.getURLSpecFromFile(file);
}

/**
 * Gets the specified directory at the specified hierarchy under a 
 * Directory Service key. 
 * @param   key
 *          The Directory Service Key to start from
 * @param   pathArray
 *          An array of path components to locate beneath the directory 
 *          specified by |key|
 * @return  nsIFile object for the location specified. If the directory
 *          requested does not exist, it is created, along with any
 *          parent directories that need to be created.
 */
function getDir(key, pathArray) {
  return getDirInternal(key, pathArray, true);
}

/**
 * Gets the specified directory at the speciifed hierarchy under a 
 * Directory Service key. 
 * @param   key
 *          The Directory Service Key to start from
 * @param   pathArray
 *          An array of path components to locate beneath the directory 
 *          specified by |key|
 * @return  nsIFile object for the location specified. If the directory
 *          requested does not exist, it is NOT created.
 */
function getDirNoCreate(key, pathArray) {
  return getDirInternal(key, pathArray, false);
}

/**
 * Gets the specified directory at the speciifed hierarchy under a 
 * Directory Service key. 
 * @param   key
 *          The Directory Service Key to start from
 * @param   pathArray
 *          An array of path components to locate beneath the directory 
 *          specified by |key|
 * @param   shouldCreate
 *          true if the directory hierarchy specified in |pathArray|
 *          should be created if it does not exist,
 *          false otherwise.
 * @return  nsIFile object for the location specified. 
 */
function getDirInternal(key, pathArray, shouldCreate) {
  var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"]
                              .getService(Components.interfaces.nsIProperties);
  var dir = fileLocator.get(key, Components.interfaces.nsIFile);
  for (var i = 0; i < pathArray.length; ++i) {
    dir.append(pathArray[i]);
    if (shouldCreate && !dir.exists())
      dir.create(nsILocalFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
  }
  return dir;
}

/**
 * Gets the file at the speciifed hierarchy under a Directory Service key.
 * @param   key
 *          The Directory Service Key to start from
 * @param   pathArray
 *          An array of path components to locate beneath the directory 
 *          specified by |key|. The last item in this array must be the
 *          leaf name of a file.
 * @return  nsIFile object for the file specified. The file is NOT created
 *          if it does not exist, however all required directories along 
 *          the way are.
 */
function getFile(key, pathArray) {
  var file = getDir(key, pathArray.slice(0, -1));
  file.append(pathArray[pathArray.length - 1]);
  return file;
}

/**
 * An enumeration of items in a JS array.
 * @constructor
 */
function ArrayEnumerator(aItems) {
  this._index = 0;
  if (aItems) {
    for (var i = 0; i < aItems.length; ++i) {
      if (!aItems[i])
        aItems.splice(i, 1);      
    }
  }
  this._contents = aItems;
}

ArrayEnumerator.prototype = {
  _index: 0,
  _contents: [],
  
  hasMoreElements: function() {
    return this._index < this._contents.length;
  },
  
  getNext: function() {
    return this._contents[this._index++];      
  }
};

/**
 * Trims a prefix from a string.
 * @param   string
 *          The source string
 * @param   prefix
 *          The prefix to remove.
 * @returns The suffix (string - prefix)
 */
function stripPrefix(string, prefix) {
  return string.substr(prefix.length);
}

/**
 * Writes a string of text to a file.  A newline will be appended to the data
 * written to the file.  This function only works with ASCII text.
 */
function writeStringToFile(file, text) {
  var fos =
      Components.classes["@mozilla.org/network/safe-file-output-stream;1"].
      createInstance(nsIFileOutputStream);
  var modeFlags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE;
  if (!file.exists()) 
    file.create(nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
  fos.init(file, modeFlags, PERMS_FILE, 0);
  text += "\n";
  fos.write(text, text.length);    
  if (fos instanceof Components.interfaces.nsISafeOutputStream)
    fos.finish();
  else
    fos.close();
}

/**
 * Reads a string of text from a file.  A trailing newline will be removed
 * before the result is returned.  This function only works with ASCII text.
 */
function readStringFromFile(file) {
  var fis =
      Components.classes["@mozilla.org/network/file-input-stream;1"].
      createInstance(nsIFileInputStream);
  var modeFlags = MODE_RDONLY;
  if (!file.exists())
    return null;
  fis.init(file, modeFlags, PERMS_FILE, 0);
  var sis =
      Components.classes["@mozilla.org/scriptableinputstream;1"].
      createInstance(Components.interfaces.nsIScriptableInputStream);
  sis.init(fis);
  var text = sis.read(sis.available());
  sis.close();
  if (text[text.length - 1] == "\n")
    text = text.slice(0, -1);
  return text;
}

/**
 *
 */
function UpdatePrompt() {
}
UpdatePrompt.prototype = {
  /**
   * See nsIUpdateService.idl
   */
  checkForUpdates: function() {
    var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                       .getService(Components.interfaces.nsIWindowMediator);
    var win = wm.getMostRecentWindow("Update:Wizard");
    if (win) {
      win.focus();
      win.checkForUpdates();
    }
    else {
      var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                         .getService(Components.interfaces.nsIWindowWatcher);
      ww.openWindow(null, URI_UPDATE_PROMPT_DIALOG, 
                    "", "chrome,centerscreen,dialog,titlebar", null);
    }
  },
    
  /**
   * See nsIUpdateService.idl
   */
  showUpdateAvailable: function(update) {
    var ary = Components.classes["@mozilla.org/supports-array;1"]
                        .createInstance(Components.interfaces.nsISupportsArray);
    ary.AppendElement(update);
      
    var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                       .getService(Components.interfaces.nsIWindowMediator);
    var win = wm.getMostRecentWindow("Update:Wizard");
    if (win) 
      win.focus();
    else {
      var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                         .getService(Components.interfaces.nsIWindowWatcher);
      ww.openWindow(null, URI_UPDATE_PROMPT_DIALOG, 
                    "", "chrome,centerscreen,dialog,titlebar", ary);
    }
  },
  
  /**
   * See nsISupports.idl
   */
  QueryInterface: function(iid) {
    if (!iid.equals(Components.interfaces.nsIUpdatePrompt) &&
        !iid.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

/**
 * UpdateService
 * A Service for managing the discovery and installation of software updates.
 */
function UpdateService() {
  gApp  = Components.classes["@mozilla.org/xre/app-info;1"]
                    .getService(Components.interfaces.nsIXULAppInfo);
  gPref = Components.classes["@mozilla.org/preferences-service;1"]
                    .getService(Components.interfaces.nsIPrefBranch2);
  gOS   = Components.classes["@mozilla.org/observer-service;1"]
                    .getService(Components.interfaces.nsIObserverService);
  gConsole = Components.classes["@mozilla.org/consoleservice;1"]
                       .getService(Components.interfaces.nsIConsoleService);  

  // Start the update timer only after a profile has been selected so that the
  // appropriate values for the update check are read from the user's profile.  
  gOS.addObserver(this, "profile-after-change", false);

  // Observe xpcom-shutdown to unhook pref branch observers above to avoid 
  // shutdown leaks.
  gOS.addObserver(this, "xpcom-shutdown", false);
}

UpdateService.prototype = {
  _downloader: null,

  observe: function(subject, topic, data) {
    switch (topic) {
    case "app-startup":
      // Resume fetching...
      this.downloadUpdate(null, true);
      break;
    case "profile-after-change":
      gOS.removeObserver(this, "profile-after-change");

      // Register a background update check timer
      var tm = 
          Components.classes["@mozilla.org/updates/timer-manager;1"]
                    .getService(Components.interfaces.nsIUpdateTimerManager);
      var interval = getPref("getIntPref", PREF_APP_UPDATE_INTERVAL, 86400000);
      tm.registerTimer("background-update-timer", this, interval,
                       Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
      break;
    case "xpcom-shutdown":
      gOS.removeObserver(this, "xpcom-shutdown");
      break;
    }
  },
  
  _needsToPromptForUpdate: function(updates) {
    // First, check for Extension incompatibilities. These trump any preference
    // settings.
    var em = Components.classes["@mozilla.org/extensions/manager;1"]
                       .getService(Components.interfaces.nsIExtensionManager);
    var incompatibleList = { };
    for (var i = 0; i < updates.length; ++i) {
      var count = {};
      em.getIncompatibleItemList(gApp.ID, updates[i].extensionversion,
                                 nsIUpdateItem.TYPE_ADDON, count);
      if (count.value > 0)
        return true;
    }

    // Now, inspect user preferences.
    
    // No prompt necessary, silently update...
    return false;
  },
  
  notify: function(timer) {
    // If a download is in progress, then do nothing.
    if (this._downloader && this._downloader.isBusy)
      return;

    var self = this;
    var listener = {
      onProgress: function() { },
      onCheckComplete: function(updates, updateCount) {
        self._selectAndInstallUpdate(updates);
      },
      onError: function() { },
    }

    this.checkForUpdates(listener);
  },
  
  /**
   * Determine whether or not an update requires user confirmation before it
   * can be installed.
   * @param   update
   *          The update to be installed
   * @returns true if a prompt UI should be shown asking the user if they want
   *          to install the update, false if the update should just be 
   *          silently downloaded and installed.
   */
  _shouldPrompt: function(update) {
    // There are two possible outcomes here:
    // 1. download and install the update automatically
    // 2. alert the user about the presence of an update before doing anything
    //
    // The outcome we follow is determined as follows:
    // 
    // Update Type      Mode      Incompatible    Outcome
    // Major            0         No              Auto Install
    // Major            0         Yes             Notify and Confirm
    // Major            1         Yes or No       Notify and Confirm
    // Major            2         Yes or No       Notify and Confirm
    // Minor            0         No              Auto Install
    // Minor            0         Yes             Notify and Confirm
    // Minor            1         No              Auto Install
    // Minor            1         Yes             Notify and Confirm
    // Minor            2         Yes or No       Notify and Confirm
    //
    // If app.update.enabled is set to false, an update check is not performed
    // at all, and so none of the decision making above is entered into.
    //
    var updateEnabled = getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true);
    if (!updateEnabled)
      return;
      
    var mode = getPref("getIntPref", PREF_APP_UPDATE_AUTOINSTALL_MODE, 0);
    var compatible = isCompatible(update);
    if ((update.type == "major" && (mode == 0)) ||
        (update.type == "minor" && (mode == 0 || mode == 1)))
      return !compatible;
    return true;
  },
  
  /**
   * Determine which of the specified updates should be installed.
   * @param   updates
   *          An array of available updates
   */
  selectUpdate: function(updates) {
    if (updates.length == 0)
      return null;
    
    // Choose the newest of the available minor and major updates. 
    var majorUpdate = null, minorUpdate = null;
    var newestMinor = updates[0], newestMajor = updates[0];
    
    var vc = new VersionChecker();
    for (var i = 0; i < updates.length; ++i) {
      if (updates[i].type == "major" && 
          vc.compare(newestMajor.version, updates[i].version) < 0)
        majorUpdate = newestMajor = updates[i];
      if (updates[i].type == "minor" && 
          vc.compare(newestMinor.version, updates[i].version) < 0)
        minorUpdate = newestMinor = updates[i];
    }
    
    // If there's a major update, always try and fetch that one first, 
    // otherwise fall back to the newest minor update.
    return majorUpdate || minorUpdate;
  },
  
  /**
   * Determine which of the specified updates should be installed and
   * begin the download/installation process, optionally prompting the
   * user for permission if required.
   * @param   updates
   *          An array of available updates
   */
  _selectAndInstallUpdate: function(updates) {
    var update = this.selectUpdate(updates, updates.length);
    if (this._shouldPrompt(update)) {
      LOG("_selectAndInstallUpdate: need to prompt user before continuing...");
      var prompter = 
          Components.classes["@mozilla.org/updates/update-prompt;1"].
          createInstance(Components.interfaces.nsIUpdatePrompt);
      prompter.showUpdateAvailable(update);
    }
    else
      this.downloadUpdate(update, true);
  },

  /**
   * 
   */
  checkForUpdates: function(listener) {
    var checker = new Checker();
    checker.findUpdates(listener);
    return checker;
  },
  
  /**
   * See nsIUpdateService.idl
   */
  addDownloadListener: function(listener) {
    if (!this._downloader) {
      LOG("addDownloadListener: no downloader!\n");
      return;
    }
    this._downloader.addDownloadListener(listener);
  },
  
  /**
   * See nsIUpdateService.idl
   */
  removeDownloadListener: function(listener) {
    if (!this._downloader) {
      LOG("removeDownloadListener: no downloader!\n");
      return;
    }
    this._downloader.removeDownloadListener(listener);
  },
  
  /**
   * See nsIUpdateService.idl
   */
  downloadUpdate: function(update, background) {
    if (this._downloader && this._downloader.isBusy) {
      if (update.isCompleteUpdate == this._downloader.isCompleteUpdate) {
        LOG("no support for downloading more than one update at a time");
        return;
      }
      this._downloader.cancel();
    }
    this._downloader = new Downloader(background);
    this._downloader.downloadUpdate(update);
  },
  
  /**
   * See nsIUpdateService.idl
   */
  pauseDownload: function() {
    if (this._downloader && this._downloader.isBusy)
      this._downloader.cancel();
  },
  
  /**
   * See nsISupports.idl
   */
  QueryInterface: function(iid) {
    if (!iid.equals(Components.interfaces.nsIApplicationUpdateService) &&
        !iid.equals(Components.interfaces.nsITimerCallback) && 
        !iid.equals(Components.interfaces.nsIObserver) && 
        !iid.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

/**
 * Gets a preference value, handling the case where there is no default.
 * @param   func
 *          The name of the preference function to call, on nsIPrefBranch
 * @param   preference
 *          The name of the preference
 * @param   defaultValue
 *          The default value to return in the event the preference has 
 *          no setting
 * @returns The value of the preference, or undefined if there was no
 *          user or default value.
 */
function getPref(func, preference, defaultValue) {
  try {
    return gPref[func](preference);
  }
  catch (e) {
  }
  return defaultValue;
}

/**
 * Update Patch
 */
function UpdatePatch(type, url, hashfunction, hashvalue, size) {
  this.type = type;
  this.url = url;
  this.hashfunction = hashfunction;
  this.hashvalue = hashvalue;
  this.size = size;
}

UpdatePatch.prototype = {
  QueryInterface: function(iid) {
    if (!iid.equals(Components.interfaces.nsIUpdatePatch) &&
        !iid.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

/**
 * Update
 * Implements nsIUpdate
 */
function Update(type, version, extensionversion, detailsurl, patches) {
  this.type = type;
  this.version = version;
  this.extensionversion = extensionversion;
  this.detailsurl = detailsurl;
  this.isCompleteUpdate = false;
  this._patches = patches;
}

Update.prototype = {
  /**
   * 
   */
  get patchCount() {
    return this._patches.length;
  },
  
  /**
   *
   */
  getPatchAt: function(index) {
    return this._patches[index];
  },
   
  /**
   * See nsISupports.idl
   */
  QueryInterface: function(iid) {
    if (!iid.equals(Components.interfaces.nsIUpdate) &&
        !iid.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
}; 
 

/**
 * ParseUpdateNode
 * Parses an <update> element into an Update object.
 */
function ParseUpdateNode(node) {
  patches = [];

  for (var i = 0; i < node.childNodes.length; ++i) {
    var patchNode = node.childNodes[i];
    if (patchNode.nodeType != Node.ELEMENT_NODE)
      continue;

    patches.push(
        new UpdatePatch(patchNode.getAttribute("type"),
                        patchNode.getAttribute("url"),
                        patchNode.getAttribute("hashfunction"),
                        patchNode.getAttribute("hashvalue"),
                        parseInt(patchNode.getAttribute("size"))));
  }

  return new Update(node.getAttribute("type"),
                    node.getAttribute("version"),
                    node.getAttribute("extensionversion"),
                    node.getAttribute("detailsurl"),
                    patches);
}

/**
 * Checker
 */
function Checker() {
}
Checker.prototype = {
  _request  : null,
  _callback : null,
  observer  : null,
  
  get _updateURL() {
    try {
      //return gPref.getComplexValue(PREF_APP_UPDATE_URL, nsIPrefLocalizedString).data;
      return gPref.getCharPref(PREF_APP_UPDATE_URL);
    }
    catch (e) {
    }
    return null;
  },
  
  findUpdates: function(callback) {
    if (!callback)
      throw Components.results.NS_ERROR_NULL_POINTER;
      
    if (!this._updateURL)
      return;
      
    this._request = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
                              .createInstance(Components.interfaces.nsIXMLHttpRequest);
    this._request.open("GET", this._updateURL, true);
    this._request.overrideMimeType("text/xml");
    this._request.setRequestHeader("Cache-Control", "no-cache");
    
    var self = this;
    this._request.onerror     = function(event) { self.onError(event);    };
    this._request.onload      = function(event) { self.onLoad(event);     };
    this._request.onprogress  = function(event) { self.onProgress(event); };

    LOG("Checker.findUpdates: sending request to " + this._updateURL);
    this._request.send(null);
    
    this._callback = callback;
  },
  
  onProgress: function(event) {
    LOG("*** download progress: " + event.position + "/" + event.totalSize);
    this._callback.onProgress(event.target, event.position, event.totalSize);
  },
  
  get _updates() {
    var updatesElement = this._request.responseXML.documentElement;
    if (!updatesElement) {
      LOG("Checker.get_updates: empty updates document?!");
      return [];
    }

    var updates = [];
    for (var i = 0; i < updatesElement.childNodes.length; ++i) {
      var updateElement = updatesElement.childNodes[i];
      if (updateElement.nodeType != Node.ELEMENT_NODE)
        continue;

      updates.push(ParseUpdateNode(updateElement));
    }

    return updates;
  },
  
  onLoad: function(event) {
    LOG("Checker.onLoad: request completed downloading document");
    
    // Notify the front end that we're complete
    if (this.observer)
      this.observer.onLoad(event.target);

    // Analyze the resulting DOM and determine the set of updates to install
    var updates = this._updates;
    
    // ... and tell the Update Service about what we discovered.
    this._callback.onCheckComplete(updates, updates.length);
  },
  
  onError: function(event) {
    LOG("Checker.onError: error during load");
    this._callback.onError(event.target);
  },
  
  /**
   * See nsIUpdateService.idl
   */
  stopChecking: function() {
    this._request.abort();
  },
  
  /**
   * See nsISupports.idl
   */
  QueryInterface: function(iid) {
    if (!iid.equals(Components.interfaces.nsIUpdateChecker) &&
        !iid.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

/**
 * Manages the download of updates
 * @param   background
 *          Whether or not this downloader is operating in background
 *          update mode. 
 * @constructor
 */
function Downloader(background) {
  this.background = background;
}
Downloader.prototype = {
  _patch: null,    // UpdatePatch
  _request: null,  // nsIIncrementalDownload

  /**
   * Whether or not the update being downloaded is a complete replacement of
   * the user's existing installation or a patch representing the difference
   * between the new version and the previous version.
   */
  isCompleteUpdate: null,

  /**
   *
   */  
  cancel: function() {
    if (this._request && 
        this._request instanceof Components.interfaces.nsIRequest) {
      const NS_BINDING_ABORTED = 0x804b0002;
      this._request.cancel(NS_BINDING_ABORTED);
    }
  },

  /**
   * 
   */
  _getUpdatesDir: function() {
    // Right now, we only support downloading one patch at a time, so we always
    // use the same target directory.
    var fileLocator =
        Components.classes["@mozilla.org/file/directory_service;1"].
        getService(Components.interfaces.nsIProperties);
    var appDir = fileLocator.get(KEY_APPDIR, Components.interfaces.nsIFile);
    appDir.append(DIR_UPDATES);
    appDir.append("0");
    if (!appDir.exists())
      appDir.create(nsILocalFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
    return appDir;
  },
  
  /**
   * Writes the current update operation/state to a file in the patch 
   * directory, indicating to the patching system that operations need
   * to be performed.
   * @param   dir
   *          The patch directory where the update.status file should be 
   *          written.
   * @param   state
   *          The state value to write.
   */
  _writeStatusFile: function(dir, state) {
    var statusFile = dir.clone();
    statusFile.append(FILE_UPDATE_STATUS);
    writeStringToFile(statusFile, state);
  },

  /**
   * Reads the current update state...
   */
  _readStatusFile: function(dir) {
    var statusFile = dir.clone();
    statusFile.append(FILE_UPDATE_STATUS);
    LOG(statusFile.path);
    return readStringFromFile(statusFile);
  },

  /**
   * Verify the downloaded file.  We assume that the download is complete at
   * this point.
   */
  _verifyDownload: function() {
    var fileStream = Components.classes["@mozilla.org/network/file-input-stream;1"].
        createInstance(nsIFileInputStream);
    fileStream.init(this._request.destination, MODE_RDONLY, PERMS_FILE, 0);

    try {
      var hash = Components.classes["@mozilla.org/security/hash;1"].
          createInstance(nsICryptoHash);
      const map = {
        "MD2"    : nsICryptoHash.MD2,
        "MD5"    : nsICryptoHash.MD5,
        "SHA1"   : nsICryptoHash.SHA1,
        "SHA256" : nsICryptoHash.SHA256,
        "SHA384" : nsICryptoHash.SHA384,
        "SHA512" : nsICryptoHash.SHA512
      };
      var hashfunction = this._patch.hashfunction.toUpperCase();
      if (!(hashfunction in map))
        return false;
      hash.init(map[hashfunction]);
      hash.updateFromStream(fileStream, -1);
      digest = binaryToHex(hash.finish(false));
    } catch (e) {
      LOG("failed to compute hash of downloaded update archive");
      digest = "";
    }

    fileStream.close();

    return digest == this._patch.hashvalue;
  },

  /**
   * Select the patch to use given the current state of updateDir and the given
   * set of update patches.
   */
  _selectPatch: function(update, updateDir) {
    // Given an update to download, we will always try to download the patch
    // for a partial update over the patch for a full update.

    // Return the first UpdatePatch with the given type.
    function getPatchOfType(type) {
      for (var i = 0; i < update.patchCount; ++i) {
        var patch = update.getPatchAt(i);
        if (patch.type == type)
          return patch;
      }
      return null;
    }

    // Return the first UpdatePatch with the given hash.
    function getPatchWithHash(hashvalue) {
      for (var i = 0; i < update.patchCount; ++i) {
        var patch = update.getPatchAt(i);
        if (patch.hashvalue == hashvalue)
          return patch;
      }
      return null;
    }

    // Deserialize the UpdatePatch currently being downloaded.
    function readUpdateInfo() {
      var infoFile = updateDir.clone();
      infoFile.append(FILE_UPDATE_INFO);
      var info = readStringFromFile(infoFile);
      if (!info)
        return null;
      var fields = info.split(" ");  

      return new UpdatePatch(fields[0], fields[1], fields[2], fields[3],
                             parseInt(fields[4]));
    }

    // Serialize the given UpdatePatch.
    function writeUpdateInfo(patch) {
      var info =
         patch.type + " " +
         patch.url + " " +
         patch.hashfunction + " " +
         patch.hashvalue + " " +
         patch.size;
      var infoFile = updateDir.clone();
      infoFile.append(FILE_UPDATE_INFO);
      writeStringToFile(infoFile, info);
    }

    // OK, now start by looking at what is currently in the updateDir.
    var state = this._readStatusFile(updateDir);
    var patch = readUpdateInfo();

    // If this is a patch that we know about, then select it.  If it is a patch
    // that we do not know about, then remove it and use our default logic.

    var useComplete = false;

    if (patch) {
      LOG("found existing patch [state="+state+"]");

      // We may have no update to compare the existing patch against.
      var found = update ? getPatchWithHash(patch.hashvalue) : patch;
      if (found) {
        LOG("existing patch is known");

        if (state == STATE_DOWNLOADING) {
          LOG("resuming download");
          return found;
        }
        if (state == STATE_PENDING) {
          LOG("already downloaded and staged");
          return null;
        }
        if (state == STATE_FAILED || state == STATE_APPLYING) {
          // Something went wrong when we tried to apply the previous patch.
          // Try the complete patch next time.
          if (update && patch.type == "partial") {
            useComplete = true;
          } else {
            // This is a pretty fatal error.  Just bail.
            LOG("failed to apply complete patch!");
            return null;
          }
        }
      }

      // The patch is no longer known to us, so we should assume that it is
      // no longer valid.  Remove all from the updateDir.

      // XXX We may want to preserve an update.log file from a failed update.

      // We expect this operation to fail since some files (e.g., updater.exe)
      // may still be in use.  That's ok.
      try {
        updateDir.remove(true);
      } catch (e) {}
      // Restore the updateDir since we may have deleted it.
      updateDir = this._getUpdatesDir();

      state = null;
      patch = null;
    }

    if (!update)
      return null;

    // Now, we select the best patch from the given set
    if (!useComplete)
      patch = getPatchOfType("partial");
    if (!patch)
      patch = getPatchOfType("complete");
      
    update.isCompleteUpdate = useComplete;

    writeUpdateInfo(patch);
    return patch;
  },

  get isBusy() {
    return this._request != null;
  },
  
  /**
   * Download and stage the given update.
   *
   * @param update
   *        An Update instance or null.  If null, then the downloader will
   *        attempt to resume downloading what it was previously downloading.
   */
  downloadUpdate: function(update) {
    var updateDir = this._getUpdatesDir();

    // This function may return null, which indicates that there are no patches
    // to download.
    this._patch = this._selectPatch(update, updateDir);
    if (!this._patch) {
      LOG("no patch to download");
      return;
    }
    this.isCompleteUpdate = this._patch.type == "complete";

    var patchFile = updateDir.clone();
    patchFile.append(FILE_UPDATE_ARCHIVE);

    var ios = Components.classes["@mozilla.org/network/io-service;1"].
        getService(Components.interfaces.nsIIOService);
    var uri = ios.newURI(this._patch.url, null, null);

    this._request =
        Components.classes["@mozilla.org/network/incremental-download;1"].
        createInstance(nsIIncrementalDownload);

    LOG("Downloader.downloadUpdate: Downloading from " + uri.spec + " to " + 
        patchFile.path);

    var interval = this.background ? DOWNLOAD_BACKGROUND_INTERVAL
                                   : DOWNLOAD_FOREGROUND_INTERVAL;
    this._request.init(uri, patchFile, DOWNLOAD_CHUNK_SIZE, interval);
    this._request.start(this, null);

    this._writeStatusFile(updateDir, STATE_DOWNLOADING);
  },
  
  _listeners: [],

  /** 
   * 
   */
  addDownloadListener: function(listener) {
    for (var i = 0; i < this._listeners.length; ++i) {
      if (this._listeners[i] == listener)
        return;
    }
    this._listeners.push(listener);
  },
  
  /** 
   *
   */
  removeDownloadListener: function(listener) {
    for (var i = 0; i < this._listeners.length; ++i) {
      if (this._listeners[i] == listener) {
        this._listeners.splice(i, 1);
        return;
      }
    }
  },
  
  onStartRequest: function(request, context) {
    request.QueryInterface(nsIIncrementalDownload);
    LOG("Downloader.onStartRequest: " + request.URI.spec);
    var listenerCount = this._listeners.length;
    for (var i = 0; i < listenerCount; ++i)
      this._listeners[i].onStartRequest(request, context);
  },
  
  onProgress: function(request, context, progress, maxProgress) {
    request.QueryInterface(nsIIncrementalDownload);
    // LOG("Downloader.onProgress: " + request.URI.spec + ", " + progress + "/" + maxProgress);
    var listenerCount = this._listeners.length;
    for (var i = 0; i < listenerCount; ++i) {
      var listener = this._listeners[i];
      if (listener instanceof Components.interfaces.nsIProgressEventSink)
        listener.onProgress(request, context, progress, maxProgress);
    }
  },
  
  onStatus: function(request, context, status, statusText) {
    request.QueryInterface(nsIIncrementalDownload);
    LOG("Downloader.onStatus: " + request.URI.spec + " status = " + status + ", text = " + statusText);
    var listenerCount = this._listeners.length;
    for (var i = 0; i < listenerCount; ++i) {
      var listener = this._listeners[i];
      if (listener instanceof Components.interfaces.nsIProgressEventSink)
        listener.onStatus(request, context, status, statusText);
    }
  },
  
  onStopRequest: function(request, context, status) {
    request.QueryInterface(nsIIncrementalDownload);
    LOG("Downloader.onStopRequest: " + request.URI.spec + ", status = " + status);

    if (Components.isSuccessCode(status)) {
      var state;
      if (this._verifyDownload()) {
        state = STATE_PENDING;
      } else {
        LOG("download verification failed");
        state = STATE_FAILED;
        // TODO: use more informative error code here
        status = Components.results.NS_ERROR_UNEXPECTED;
      }
      this._writeStatusFile(this._getUpdatesDir(), state);
    }

    var listenerCount = this._listeners.length;
    for (var i = 0; i < listenerCount; ++i)
      this._listeners[i].onStopRequest(request, context, status);

    this._request = null;
  },
   
  /**
   * See nsISupports.idl
   */
  QueryInterface: function(iid) {
    if (!iid.equals(Components.interfaces.nsIRequestObserver) &&
        !iid.equals(Components.interfaces.nsIProgressEventSink) &&
        !iid.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

function TimerManager() {
}
TimerManager.prototype = {
  _timers: { },
  registerTimer: function(id, callback, interval, type) {
    const nsITimer = Components.interfaces.nsITimer;
    var timer = Components.classes["@mozilla.org/timer;1"]
                          .createInstance(nsITimer);
    var timerInterval = getPref("getIntPref", PREF_APP_UPDATE_TIMER, 5000);
    
    var self = this;
    function TimerCallback(id, callback, interval) {
      this.id = id;
      this.callback = callback;
      this.interval = interval;
    }
    TimerCallback.prototype = {
      notify: function(timer) {
        LOG("self._timers = " + self._timers.toSource());
        var lastUpdateTime = self._timers[this.id].lastUpdateTime;
        var now = Math.round(Date.now() / 1000);
        LOG("notify = " + (now - lastUpdateTime) + " > " + this.interval);
        if ((now - lastUpdateTime) > this.interval &&
            this.callback instanceof Components.interfaces.nsITimerCallback) {
          this.callback.notify(timer);
          self._timers[this.id].lastUpdateTime = now;
          var preference = PREF_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, this.id);
          gPref.setIntPref(preference, now);
        }
      },

      /**
       * See nsISupports.idl
       */
      QueryInterface: function(iid) {
        if (!iid.equals(Components.interfaces.nsITimerCallback) &&
            !iid.equals(Components.interfaces.nsISupports))
          throw Components.results.NS_ERROR_NO_INTERFACE;
        return this;
      }
    };
    var tc = new TimerCallback(id, callback, interval);
    LOG("timerInterval = " + timerInterval);
    timer.initWithCallback(tc, timerInterval, type);
    var preference = PREF_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, id);
    var lastUpdateTime = getPref("getIntPref", preference, 
                                 Math.round(Date.now() / 1000));
    this._timers[id] = { timer: timer, lastUpdateTime: lastUpdateTime };
  },

  QueryInterface: function(iid) {
    if (!iid.equals(Components.interfaces.nsIUpdateTimerManager) &&
        !iid.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

function Version(aMajor, aMinor, aRelease, aBuild, aPlus) 
{ 
  this.major    = aMajor    || 0;
  this.minor    = aMinor    || 0;
  this.release  = aRelease  || 0;
  this.build    = aBuild    || 0;
  this.plus     = aPlus     || 0;
}

Version.prototype = {
  toString: function Version_toString() 
  {
    return this.major + "." + this.minor + "." + this.subminor + "." + this.release + (this.plus ? "+" : "");
  },
  
  compare: function (aVersion)
  {
    var fields = ["major", "minor", "release", "build", "plus"];
    
    for (var i = 0; i < fields.length; ++i) {
      var field = fields[i];
      if (aVersion[field] > this[field])
        return -1;
      else if (aVersion[field] < this[field])
        return 1;
    }
    return 0;
  }
}

function VersionChecker() {
}

VersionChecker.prototype = {
  /////////////////////////////////////////////////////////////////////////////
  // nsIVersionChecker
  
  // -ve      if B is newer
  // equal    if A == B
  // +ve      if A is newer
  compare: function(aVersionA, aVersionB)
  {
    var a = this._decomposeVersion(aVersionA);
    var b = this._decomposeVersion(aVersionB);
    
    return a.compare(b);
  },
  
  _decomposeVersion: function(aVersion)
  {
    var plus = 0;
    if (aVersion.charAt(aVersion.length-1) == "+") {
      aVersion = aVersion.substr(0, aVersion.length-1);
      plus = 1;
    }

    var parts = aVersion.split(".");
    
    return new Version(this._getValidInt(parts[0]),
                       this._getValidInt(parts[1]),
                       this._getValidInt(parts[2]),
                       this._getValidInt(parts[3]),
                       plus);
  },
  
  _getValidInt: function(aPartString)
  {
    var integer = parseInt(aPartString);
    if (isNaN(integer))
      return 0;
    return integer;
  },
  
  isValidVersion: function(aVersion)
  {
    return /^([0-9]+\.){0,3}[0-9]+\+?$/.test(aVersion);
  },

  /////////////////////////////////////////////////////////////////////////////
  // nsISupports
  QueryInterface: function(aIID) 
  {
    if (!aIID.equals(Components.interfaces.nsIVersionChecker) &&
        !aIID.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

var gModule = {
  registerSelf: function(componentManager, fileSpec, location, type) {
    componentManager = componentManager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
    
    for (var key in this._objects) {
      var obj = this._objects[key];
      componentManager.registerFactoryLocation(obj.CID, obj.className, obj.contractID,
                                               fileSpec, location, type);
    }

    // Make the Update Service a startup observer
    var categoryManager = Components.classes["@mozilla.org/categorymanager;1"]
                                    .getService(Components.interfaces.nsICategoryManager);
    categoryManager.addCategoryEntry("app-startup", this._objects.service.className,
                                     "service," + this._objects.service.contractID, 
                                     true, true, null);
  },
  
  getClassObject: function(componentManager, cid, iid) {
    if (!iid.equals(Components.interfaces.nsIFactory))
      throw Components.results.NS_ERROR_NOT_IMPLEMENTED;

    for (var key in this._objects) {
      if (cid.equals(this._objects[key].CID))
        return this._objects[key].factory;
    }
    
    throw Components.results.NS_ERROR_NO_INTERFACE;
  },
  
  _makeFactory: #1= function(ctor) {
    function ci(outer, iid) {
      if (outer != null)
        throw Components.results.NS_ERROR_NO_AGGREGATION;
      return (new ctor()).QueryInterface(iid);
    } 
    return { createInstance: ci };
  },
  
  _objects: {
    service: { CID        : Components.ID("{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}"),
               contractID : "@mozilla.org/updates/update-service;1",
               className  : "Update Service",
               factory    : #1#(UpdateService)
             },
    prompt:  { CID        : Components.ID("{27ABA825-35B5-4018-9FDD-F99250A0E722}"),
               contractID : "@mozilla.org/updates/update-prompt;1",
               className  : "Update Prompt",
               factory    : #1#(UpdatePrompt)
             },
    version: { CID        : Components.ID("{9408E0A5-509E-45E7-80C1-0F35B99FF7A9}"),
               contractID : "@mozilla.org/updates/version-checker;1",
               className  : "Version Checker",
               factory    : #1#(VersionChecker)
             },
    timers:  { CID        : Components.ID("{B322A5C0-A419-484E-96BA-D7182163899F}"),
               contractID : "@mozilla.org/updates/timer-manager;1",
               className  : "Timer Manager",
               factory    : #1#(TimerManager)
             }
  },
  
  canUnload: function(componentManager) {
    return true;
  }
};

function NSGetModule(compMgr, fileSpec) {
  return gModule;
}

/**
 * Determines whether or there are installed addons which are incompatible 
 * with this update.
 * @param   update
 *          The update to check compatibility against
 * @returns true if there are no addons installed that are incompatible with
 *          the specified update, false otherwise.
 */
function isCompatible(update) {
//@line 1411 "/cygdrive/d/builds/tinderbox/XR-Trunk/WINNT_5.0_Depend/mozilla/toolkit/mozapps/update/src/nsUpdateService.js.in"
}

