// dataSync.uc.js-0.6.7:
//   synchronize bookmarks, cookies, permissions, and stylish with a remote
//   server

// PHP example:
/*
-- begin cut ------------------------------------------------------------------
<?
  // authentication and/or IP restrictions are recommended
  $user = getenv('REMOTE_USER');

  $head = substr($HTTP_RAW_POST_DATA, 0, 256);

  $type = "";
  $serial = "";
  $hash = "";

  if (strstr($head, "<dataSync ")) {
    if (ereg(' type=["\']([^"\']+)["\']', $head, $regs))
      $type = basename($regs[1]);

    if (ereg(' serial=["\']([0-9]+)["\']', $head, $regs))
      $serial = $regs[1];

    if (ereg(' hash=["\']([^"\']+)["\']', $head, $regs))
      $hash = $regs[1];
  }

  function sendRes($type, $success, $message, $serial = "") {
    echo("<dataSync type='$type' success='$success' message='$message'" .
           (($success && $serial) ? " serial='$serial'" : "") . "/>");
  }

  header("Content-type: application/xml");

  if (empty($type) || empty($serial)) {
    sendRes($type, 0, "Missing type or serial");
    exit;
  }

  if (!ereg("^(updates|bookmarks|permissions|cookies|stylish)$", $type)) {
    sendRes($type, 0, "Unknown type");
    exit;
  }

  // directory (and files) must be writable by the server and
  // should be protected from (unauthorized) GET requests
  $file = "data/" . ($user ? "$user-" : "") . $type . ".xml";

  if ($type == 'updates') {
    if (file_exists($file))
      readfile($file);
    exit;
  }

  // contains a hash, data export
  if ($hash) {
    if (file_exists($file)) {
      $old = "$file.old";
      if (file_exists($old))
        unlink($old);
      rename($file, $old);
    }

    if ($fp = fopen($file, "w")) {
      $bytes = fputs($fp, $HTTP_RAW_POST_DATA);
      fclose($fp);

      $len = strlen($HTTP_RAW_POST_DATA);

      if ($bytes == $len) {
        $file = "data/" . ($user ? "$user-" : "") . "updates.xml";

        $data = "";

        if (file_exists($file))
          $data = file_get_contents($file);

        if (!$data)
          $data = "<dataSync type='updates'>\n" .
                  "  <bookmarks>0</bookmarks>\n" .
                  "  <permissions>0</permissions>\n" .
                  "  <cookies>0</cookies>\n" .
                  "  <stylish>0</stylish>\n" .
                  "</dataSync>\n";

        if (ereg("<$type>([0-9]+)<\/$type>", $data, $regs)) {
          $vers = $regs[1];

          $data = str_replace("<$type>$vers</$type>",
                              "<$type>$serial</$type>",
                              $data);
        } else
          $data = str_replace("</dataSync>",
                              "  <$type>$serial</$type>\n" . 
                              "</dataSync>",
                              $data);

        if ($fp = fopen($file, "w")) {
          fputs($fp, $data);
          fclose($fp);
        }

        sendRes($type, 1, "$bytes bytes written", $serial);
      } else
        sendRes($type, 0, "Incomplete write ($bytes of $len bytes)");
    } else
      sendRes($type, 0, "Failed to open $file");
  } else if (file_exists($file))
    readfile($file);
?>
-- end cut --------------------------------------------------------------------
*/

var dataSync = {
  urlPref:  'datasync.server.url',
  //
  // the application to POST the xml data to. see example PHP script
  // above. protecting this (and the data) with a password and/or IP
  // restrictions is recommended.

  // **************************** IMPORTANT ****************************
  //
  userPref: 'datasync.server.user',
  passPref: 'datasync.server.pass',
  //
  // if the server requires authentication, these prefs can (but don't
  // have to) be set. they will be stored in clear text in prefs.js, so
  // important credentials should not be used.
  //
  // the most secure method would be to use the password manager (with
  // a master password).

  // **************************** IMPORTANT ****************************
  //
  keyPref:  'datasync.encryption.key',
  //
  // if this pref is set, an RC4 passphrase-based encryption will be
  // used to *obfuscate* data stored on the remote server. this method
  // is useful when security is not a concern (the key is stored in
  // clear text in prefs.js) and when working with multiple profiles.
  //
  // the most secure method would be to leave this pref unset, causing
  // the secret decoder ring to be used. this is problematic when
  // working with multiple profiles as the key is unique to the profile.
  // encryption across different profiles will fail unless the same
  // key3.db is used on each.

  updateIntervalPref: 'datasync.update.interval',
  // the interval (in seconds) to check for updates (default: 3 hours),
  // 0 to disable.
  //
  // when a new version is found, a notification bar will be shown in
  // the active (or most recent) browser window.

  updatesBranchPref: 'datasync.updates.',

  handlers: [],

  rc4: function(k, p)
  {
    var s = new Array();
    var i;

    for (i = 0; i < 256; i++)
      s[i] = i;

    var j = 0;
    var x;

    for (i = 0; i < 256; i++) {
      j = (j + s[i] + k.charCodeAt(i % k.length)) % 256;
      x = s[i];
      s[i] = s[j];
      s[j] = x;
    }

    i = 0;
    j = 0;

    var c = '';

    for (var y = 0; y < p.length; y++) {
      i = (i + 1) % 256;
      j = (j + s[i]) % 256;
      x = s[i];
      s[i] = s[j];
      s[j] = x;
      c += String.fromCharCode(p.charCodeAt(y) ^ s[(s[i] + s[j]) % 256]);
    }

    return c;
  },
  encrypt: function(text)
  {
    try {
      if (this.key)
        return btoa(this.rc4(this.key, text));
      return this.sdr.encryptString(text);
    } catch (e) { alert(e); return null; }
  },
  decrypt: function(data)
  {
    try {
      if (this.key)
        return this.rc4(this.key, atob(data));
      return this.sdr.decryptString(data);
    } catch (e) { alert(e); return null; }
  },

  binToHex: function(input)
  {
    return [('0' + input.charCodeAt(i).toString(16)).slice(-2)
            for (i in input)].join('');
  },
  getStringHash: function(str)
  {
    var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                      .createInstance(Ci.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-8";

    var result = {};
    var data = converter.convertToByteArray(str, result);

    var hasher = Cc["@mozilla.org/security/hash;1"]
                   .createInstance(Ci.nsICryptoHash);
    hasher.initWithString("md5");
    hasher.update(data, data.length);

    return this.binToHex(hasher.finish(false));
  },

  createXML: function(type)
  {
    var xml = document.implementation.createDocument("", "dataSync", null);

    xml.firstChild.setAttribute('type', type);
    xml.firstChild.setAttribute('serial', this.getSerial());

    return xml;
  },
  sendXML: function(xml, isImport, forceCheck)
  {
    var req = new XMLHttpRequest();

    req.open('POST', this.url, true, this.user, this.pass);

    if (isImport)
      req.onreadystatechange = this.importStateChange;
    else
      req.onreadystatechange = this.exportStateChange;

    if (forceCheck)
      req._manualUpdateCheck = forceCheck;

    req.setRequestHeader("Content-type", "application/xml");
    req.setRequestHeader("Connection", "close");

    req.send(xml);
  },

  importStateChange: function()
  {
    if (this && this.readyState == 4) {
      if (this.status == 200) {
        var xml = this.responseXML;

        if (!xml || xml.firstChild.nodeName != 'dataSync') {
          dataSync.notificationError(null,
                   'import failed: empty document or incorrect root node');
          return;
        }

        var error = xml.firstChild.getAttribute('error');

        if (error) {
          dataSync.notificationError(null, 'import failed: ' + error);
          return;
        }

        var type = xml.firstChild.getAttribute('type');

        if (type == 'updates') {
          var label = '';

          var count = 0;

          for (var t in dataSync.handlers) {
            var s = dataSync.getPref('Int', dataSync.updatesBranchPref + t, 0);

            var k = xml.getElementsByTagName(t);

            if (k && k[0])
              k = k[0].firstChild.data;

            if (k && k > 0 && s < k) {
              dataSync.notificationUpdate(t);
              count++;
            }
          }

          //dataSync.log('upateCheck() finished: ' + count + ' update(s)');

          if (!count && this._manualUpdateCheck)
            dataSync.notificationMessage(type, 'no updates');

          return;
        }

        var handler = dataSync.getHandler(type);

        if (!handler) {
          dataSync.notificationError(type, 'import failed: unknown type');
          return;
        }

        var serial = xml.firstChild.getAttribute('serial');

        if (!serial || serial < 1) {
          dataSync.notificationError(type, 'import failed: missing serial');
          return;
        }

        var hash = xml.firstChild.getAttribute('hash');

        if (!hash) {
          dataSync.notificationError(type, 'import failed: missing hash');
          return;
        }

        if (!xml.firstChild.childNodes.length) {
          dataSync.notificationError(type, 'import failed: no child nodes');
          return;
        }

        var data = xml.firstChild.textContent;

        if (!data) {
          dataSync.notificationError(type, 'import failed: missing data');
          return;
        }

        var text = dataSync.decrypt(data);

        if (!text) {
          dataSync.notificationError(type,
                                     'import failed: unable to decrypt data');
          return;
        }

        if (dataSync.getStringHash(text) != hash) {
          dataSync.notificationError(type, 'import failed: incorect hash');
          return;
        }

        var res = handler.import(text);

        var message = 'import ';

        if (res.status) {
          dataSync.setPref('Int', dataSync.updatesBranchPref + type, serial);
          message += 'succeeded';
        } else
          message += 'failed';
      
        if (res.message)
          message += ': ' + res.message;

        dataSync.notificationMessage(type, message);
      } else
        dataSync.notificationError(type, 'import failed: server returned ' +
                                          this.status);
    }
  },
  exportStateChange: function()
  {
    if (this && this.readyState == 4) {
      if (this.status == 200) {
        var xml = this.responseXML;

        if (!xml || xml.firstChild.nodeName != 'dataSync') {
          dataSync.notificationError(null,
                       'export failed: empty document or incorrect root node');
          return;
        }

        var type    = xml.firstChild.getAttribute('type');
        var success = parseInt(xml.firstChild.getAttribute('success'));
        var message = xml.firstChild.getAttribute('message');

        var text = (message) ? ': ' + message : '';

        if (success) {
          dataSync.notificationMessage(type, 'export successful' + text);

          dataSync.setPref('Int', dataSync.updatesBranchPref + type,
                           xml.firstChild.getAttribute('serial'));
        } else
          dataSync.notificationError(type, 'export failed' + text);
      } else
        dataSync.notificationError(type, 'export failed: server returned ' +
                                          this.status);
    }
  },

  importData: function(type)
  {
    if (!this.url) {
      alert(this.urlPref + ' must be set.');
      return;
    }

    if (!this.getHandler(type)) {
      this.notificationError(type, 'import failed: unknown type');
      return;
    }

    if (confirm('Replace current ' + type + '. Are you sure?')) {
      var xml = this.createXML(type);
      this.sendXML(xml, true);
    }
  },
  exportData: function(type)
  {
    if (!this.url) {
      alert(this.urlPref + ' must be set.');
      return;
    }

    var handler = this.getHandler(type);

    if (!handler) {
      this.notificationError(type, 'export failed: unknown type');
      return;
    }

    if (!confirm('Export ' + type + '. Are you sure?'))
      return;

    var xml = this.createXML(type);

    var res = handler.export();

    if (!res.status) {
      var message = 'export failed' + (res.message ? ': ' + res.message : '');
      this.notificationError(type, message);
      return;
    }

    if (!res.data) {
      this.notificationError(type, 'export failed: no data returned for ' +
                                   type);
      return;
    }

    var hash = this.getStringHash(res.data);

    if (!hash) {
      this.notificationError(type, 'export failed: failed to compute hash ' +
                                   'for ' + type);
      return;
    }

    xml.firstChild.setAttribute('hash', hash);

    var data = this.encrypt(res.data);

    if (!data) {
      this.notificationError(type, 'export failed: failed to encrypt data ' +
                                   'for ' + type);
      return;
    }

    xml.firstChild.appendChild(xml.createTextNode(data));

    this.sendXML(xml);
  },

  observe: function(subject, topic, data)
  {
    if (topic != 'nsPref:changed')
      return;

    switch (data) {
      case this.urlPref:
        this.url  = this.getPref('Char', this.urlPref,  null);
        break;
      case this.userPref:
        this.user = this.getPref('Char', this.userPref, null);
        break;
      case this.passPref:
        this.pass = this.getPref('Char', this.passPref, null);
        break;
      case this.keyPref:
        this.key  = this.getPref('Char', this.keyPref,  null);
        break;
      case this.updateIntervalPref:
        this.updateInterval = this.getUpdateInterval();
        this.startAutoUpdateTimer();
        break;
      default:
        break;
    }
  },

  getActiveWindow: function()
  {
    /*return Cc["@mozilla.org/embedcomp/window-watcher;1"]
             .getService(Ci.nsIWindowWatcher)
             .activeWindow;*/
    return Cc["@mozilla.org/appshell/window-mediator;1"]
             .getService(Ci.nsIWindowMediator)
             .getMostRecentWindow("navigator:browser");
  },

  startAutoUpdateTimer: function()
  {
    if (this.updateTimer)
      clearTimeout(this.updateTimer);

    if (this.updateInterval > 0)
      this.updateTimer = setTimeout(function(dataSync) {
                                      dataSync.autoUpdateCheck();
                                    }, this.updateInterval * 1000, this);
  },
  autoUpdateCheck: function()
  {
    if (this.updateInterval < 1)
      return;

    var win = this.getActiveWindow();

    if (win == window) {
      var last = this.getPref('Int', this.updatesBranchPref + 'lastcheck', 0);

      var serial = this.getSerial();

      if ((last + this.updateInterval) < serial)
        this.updateCheck(serial, false);
    }

    // (re)start the timer
    this.startAutoUpdateTimer();
  },
  updateCheck: function(serial, force)
  {
    //this.log('updateCheck()');

    if (serial < 1)
      serial = this.getSerial();

    this.setPref('Int', this.updatesBranchPref + 'lastcheck', serial);

    var xml = this.createXML('updates');
    this.sendXML(xml, true, force);

    if (force)
      this.startAutoUpdateTimer();
  },

  notification: function(noticeType, type, text, buttons, delay)
  {
    var win = this.getActiveWindow();

    if (win != window)
      return;

    var notificationBox = win.getBrowser().getNotificationBox();

    if (notificationBox.getNotificationWithValue(messageType))
      return;

    var mode       = null;
    var icon       = 'moz-icon://stock/gtk-dialog-info?size=menu';
    const priority = notificationBox.PRIORITY_INFO_MEDIUM;

    switch (noticeType) {
      case 0: /* error */
        mode     = 'error';
        icon     = 'moz-icon://stock/gtk-dialog-warning?size=menu';
        priority = notificationBox.PRIORITY_WARNING_MEDIUM;
        break;
      case 1: /* message */
        mode     = 'status';
        break;
      case 2: /* update */
        mode     = 'update';
        icon     = 'chrome://mozapps/skin/update/update.png';
        break;
      default:
        break;
    }

    var message = 'dataSync: ' + (type ? type + ': ' : '') + text;

    var messageType = 'dataSync-' + mode + '-' + (type ? type : 'global');

    var notice = notificationBox.getNotificationWithValue(messageType);

    if (!notice)
      notificationBox.appendNotification(message, messageType,
                                         icon, priority, buttons);

    if (delay > 0)
      setTimeout(function(notificationBox, messageType) {
        var notice = notificationBox.getNotificationWithValue(messageType);

        if (notice)
          notificationBox.removeNotification(notice);
        }, delay * 1000, notificationBox, messageType);
  },
  notificationError: function(type, text)
  {
    this.notification(0, type, text, null, 0);
  },
  notificationMessage: function(type, text)
  {
    this.notification(1, type, text, null, 3);
  },
  notificationUpdate: function(type)
  {
    var func = 'dataSync.importData("' + type + '");';

    var callback = function() { eval(func); }

    var buttons = [{ label: 'Update',
                     accessKey: null,
                     callback: callback }];

    this.notification(2, type, 'new version available', buttons, 0);
  },

  getPref: function(type, pref, def)
  {
    try { return this.prefs['get' + type + 'Pref'](pref); } catch (e) { }

    return def;
  },
  setPref: function(type, pref, val)
  {
    try {
      this.prefs['set' + type + 'Pref'](pref, val);
    } catch (e) { alert(e); }

    this.prefs.savePrefFile(null);
  },

  getUpdateInterval: function()
  {
    var interval = this.getPref('Int', this.updateIntervalPref, 10800);

    if (interval > 0 && interval < 900)
      interval = 900

    return interval;
  },

  getSerial: function()
  {
    return parseInt(new Date().getTime() / 1000);
  },

  log: function(text)
  {
    Cc["@mozilla.org/consoleservice;1"]
      .getService(Ci.nsIConsoleService)
      .logStringMessage(new Date().toLocaleString() + ': dataSync: ' + text);
  },

  getHandler: function(type)
  {
    for (var i in this.handlers)
      if (type == i)
        return this.handlers[i];

    return null;
  },

  registerHandler: function(handler)
  {
    if (handler.type && handler.name && handler.import && handler.export) {
      if (handler.startup)
        if (!handler.startup())
          return false;

      this.handlers[handler.type] = handler;

      return true;
    }

    return false;
  },
  unregisterHandler: function(handler)
  {
    if (handler.type) {
      delete this.handlers[handler.type];

      if (handler.shutdown)
        handler.shutdown();
    }
  },

  createMenuItem: function(label, image, command)
  {
    var item = document.createElement('menuitem');
    item.setAttribute('label', label);
    item.setAttribute('image', 'moz-icon://stock/gtk-' + image + '?size=menu');
    item.setAttribute('oncommand', command);
    return item;
  },
  menu: function(event)
  {
    var popup = event.target;

    while (popup.hasChildNodes())
      popup.removeChild(popup.firstChild);

    for (var i in this.handlers) {
      var h = this.handlers[i];

      if (this.getPref('Bool', 'datasync.menu.' + i, true)) {
        if (popup.childNodes.length) {
          var item = document.createElement('menuseparator');
          popup.appendChild(item);
        }

        var item = this.createMenuItem('Import ' + h.name, h.icon,
                                         'dataSync.importData("' + i + '");');
        popup.appendChild(item);

        var item = this.createMenuItem('Export ' + h.name, 'save-as',
                                         'dataSync.exportData("' + i + '");');
        popup.appendChild(item);
      }
    }

    var item = document.createElement('menuseparator');
    popup.appendChild(item);

    var item = this.createMenuItem('Check for Updates...',
                                   'preferences',
                                   'dataSync.updateCheck(-1, true);');
    popup.appendChild(item);
  },

  startup: function()
  {
    this.prefs = Cc["@mozilla.org/preferences-service;1"]
                   .getService(Ci.nsIPrefService);

    this.sdr = Cc["@mozilla.org/security/sdr;1"]
                 .getService(Ci.nsISecretDecoderRing);

    this.pbi = Cc["@mozilla.org/preferences-service;1"]
                 .getService(Ci.nsIPrefBranch2);

    this.pbi.addObserver(this.urlPref,  this, false);
    this.pbi.addObserver(this.userPref, this, false);
    this.pbi.addObserver(this.passPref, this, false);
    this.pbi.addObserver(this.keyPref,  this, false);

    this.pbi.addObserver(this.updateIntervalPref, this, false);

    this.url  = this.getPref('Char', this.urlPref,  null);
    this.user = this.getPref('Char', this.userPref, null);
    this.pass = this.getPref('Char', this.passPref, null);
    this.key  = this.getPref('Char', this.keyPref,  null);

    this.updateInterval = this.getUpdateInterval();

    // short delay before initial check / starting main timer
    setTimeout(function(dataSync) {
                 dataSync.autoUpdateCheck();
               }, 5000, this);

    // add menu items

    var sep = document.getElementById('devToolsSeparator');

    var menu = document.createElement('menu');
    menu.setAttribute('id', 'dataSync');
    menu.setAttribute('label', 'Data Sync');
    menu.setAttribute('image', 'moz-icon://stock/gtk-network?size=menu');
    sep.parentNode.insertBefore(menu, sep);

    var popup = document.createElement("menupopup");
    popup.setAttribute('onpopupshowing', 'dataSync.menu(event);');
    menu.appendChild(popup);

    window.addEventListener("unload", function() {
                                        dataSync.shutdown();
                                      }, false);
  },
  shutdown: function()
  {
    if (this.updateTimer)
      clearTimeout(this.updateTimer);

    this.pbi.removeObserver(this.urlPref,  this)
    this.pbi.removeObserver(this.userPref, this);
    this.pbi.removeObserver(this.passPref, this);
    this.pbi.removeObserver(this.keyPref,  this);

    this.pbi.removeObserver(this.updateIntervalPref, this);
  }
};

dataSync.startup();


// builtin handlers

var dataSync_bookmarks = {
  type: 'bookmarks',

  name: 'Bookmarks',
  icon: 'about',

  startup: function()
  {
    this.file = Cc["@mozilla.org/file/directory_service;1"]
                  .getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
    this.file.append("dataSync.bookmarks");

    return true;
  },

  import: function(text)
  {
    PlacesUtils.restoreBookmarksFromJSONString(text, true);

    return { status: true, message: null };
  },
  export: function()
  {
    var file = this.file;

    var res = { status: false, message: null, data: null };

    if (file.exists())
      try {
        file.remove(false);
      } catch (e) {
        res.message = 'failed to remove ' + file.leafName + ': ' + e + '.';
        return res;
      }

    PlacesUtils.backupBookmarksToFile(file);

    if (!file.exists()) {
      res.message = 'failed to write bookmarks data';
      return res;
    }

    var stream = Cc["@mozilla.org/network/file-input-stream;1"]
                   .createInstance(Ci.nsIFileInputStream);
    stream.init(file, 0x01, 0, 0);

    var script = Cc["@mozilla.org/scriptableinputstream;1"]
                   .createInstance(Ci.nsIScriptableInputStream);
    script.init(stream);

    var text = '';

    if (stream.available()) {
      var data = script.read(4096);

      while (data.length > 0) {
        text += data;
        data = script.read(4096);
      }
    }

    stream.close();
    script.close();

    try {
      file.remove(false);
    } catch (e) {
      res.message = 'failed to remove ' + file.leafName + ': ' + e + '.';
    }

    res.status = true;
    res.data = text;

    return res;
  }
};

var dataSync_cookies = {
  type: 'cookies',

  name: 'Cookies',
  icon: 'new',

  import: function(text)
  {
    var nodes = new DOMParser().parseFromString(text, "text/xml");

    var cookies = nodes.getElementsByTagName('cookie');

    if (!cookies.length)
      return { status: false, message: 'no cookies found' };

    var cm = Cc["@mozilla.org/cookiemanager;1"]
               .getService(Ci.nsICookieManager);

    cm.removeAll();

    var cm = Cc["@mozilla.org/cookiemanager;1"]
               .getService(Ci.nsICookieManager2);

    for (var i = 0; i < cookies.length; i++) {
      var c = cookies[i];

      var expires = parseInt(c.getAttribute('expires')),
          host    = c.getAttribute('host'),
          secure  = c.getAttribute('secure') == '1',
          name    = c.getAttribute('name'),
          path    = c.getAttribute('path'),
          value   = c.getAttribute('value');

      if (expires > 1 && host && name && path && value)
        cm.add(host, path, name, value, secure, false, false, expires);
    }

    return { status: true, message: cookies.length + ' imported' };
  },
  export: function()
  {
    var nodes = document.createElement('nodes');

    var res = { status: false, message: null, data: null };

    var cm = Cc["@mozilla.org/cookiemanager;1"]
               .getService(Ci.nsICookieManager);

    var list = cm.enumerator;

    while (list.hasMoreElements()) {
      var cookie = list.getNext()
                       .QueryInterface(Ci.nsICookie);

      if (cookie.expires > 1) {
        var e = document.createElement('cookie');
        e.setAttribute('expires', cookie.expires);
        e.setAttribute('host',    cookie.host);
        e.setAttribute('secure',  cookie.isSecure ? 1 : 0);
        e.setAttribute('name',    cookie.name);
        e.setAttribute('path',    cookie.path);
        e.setAttribute('value',   cookie.value);
        nodes.appendChild(e);
      }
    }

    if (nodes.childNodes.length) {
      res.status = true;
      res.data = new XMLSerializer().serializeToString(nodes);
    } else
      res.message = 'no cookies found';

    return res;
  }
};

var dataSync_permissions = {
  type: 'permissions',

  name: 'Permissions',
  icon: 'dialog-authentication',

  import: function(text)
  {
    var nodes = new DOMParser().parseFromString(text, "text/xml");

    var perms = nodes.getElementsByTagName('perm');

    if (!perms.length)
      return { status: false, message: 'no permissions found' };

    var ios = Cc["@mozilla.org/network/io-service;1"]
                .getService(Ci.nsIIOService);

    var pm = Cc["@mozilla.org/permissionmanager;1"]
               .getService(Ci.nsIPermissionManager);

    pm.removeAll();

    for (var i = 0; i < perms.length; i++) {
      var p = perms[i];

      var host = p.getAttribute('host'),
          type = p.getAttribute('type'),
          perm = parseInt(p.getAttribute('perm'));

      if (host && type && (perm >= 0 && perm <= 2)) {
        var uri = ios.newURI('http://' + host, null, null);
        pm.add(uri, type, perm);
      }
    }

    return { status: true, message: perms.length + ' imported' };
  },
  export: function()
  {
    var nodes = document.createElement('nodes');

    var res = { status: false, message: null, data: null };

    var pm = Cc["@mozilla.org/permissionmanager;1"]
               .getService(Ci.nsIPermissionManager);

    var list = pm.enumerator;

    while (list.hasMoreElements()) {
      var perm = list.getNext()
                     .QueryInterface(Ci.nsIPermission);

      var e = document.createElement('perm');
      e.setAttribute('host', perm.host);
      e.setAttribute('type', perm.type);
      e.setAttribute('perm', perm.capability);
      nodes.appendChild(e);
    }

    if (nodes.childNodes.length) {
      res.status = true;
      res.data = new XMLSerializer().serializeToString(nodes);
    } else
      res.message = 'no permissions found';

    return res;
  }
};

var dataSync_stylish = {
  type: 'stylish',

  name: 'Stylish',
  icon: 'bold',

  startup: function()
  {
    try {
      this.stylish = Cc["@userstyles.org/style;1"]
                       .getService(Ci.stylishStyle);
      return true;
    } catch (e) { }

    return false;
  },

  import: function(text)
  {
    var nodes = new DOMParser().parseFromString(text, "text/xml");

    var styles = nodes.getElementsByTagName('style');

    if (!styles.length)
      return { status: false, message: 'no styles found' };

    // XXX: shouldn't happen
    if (!this.stylish)
      return { status: false, message: 'failed to get stylish component' };

    var total = styles.length;

    var list = [];

    for (var i = 0; i < styles.length; i++) {
      var style = styles[i];

      var s = {};

      s.url          = style.getAttribute('url');
      s.updateUrl    = style.getAttribute('updateUrl');
      s.md5Url       = style.getAttribute('md5Url');
      s.name         = style.getAttribute('name');
      s.enabled      = style.getAttribute('enabled');
      s.enabled      = (s.enabled == 'true') ? true : false;

      var c = style.getElementsByTagName('code');
      s.code = c[0].textContent;

      var c = style.getElementsByTagName('originalCode');

      if (c && c.length)
        s.originalCode = c[0].textContent;

      var h = dataSync.getStringHash(s.url + s.updateUrl +
                                     s.name + s.code);

      list[h] = s;
    }

    styles = list;

    this.stylish.list(0, {}).forEach(function(s) {
        var h = dataSync.getStringHash(s.url + s.updateUrl +
                                       s.name + s.code);

        var n = styles[h];

        if (n && n.enabled == s.enabled)
          delete styles[h];
        else
          s.delete();
      });

    var mode = this.stylish.CALCULATE_META |
                 this.stylish.REGISTER_STYLE_ON_CHANGE;

    var count = 0;

    for (var h in styles) {
      var s = styles[h];

      var style = Cc["@userstyles.org/style;1"]
                    .createInstance(Ci.stylishStyle);

      style.mode = mode;
      style.init(s.url, s.updateUrl, s.md5Url, s.name,
                 s.code, s.enabled, s.originalCode);
      style.save();

      count++;
    }

    if (count)
      Cc["@mozilla.org/observer-service;1"]
        .getService(Ci.nsIObserverService)
        .notifyObservers(null, "stylish-style-change", null);

    return { status: true, message: total + ' processed, ' +
                                    count + ' imported' };
  },
  export: function()
  {
    var res = { status: false, message: null, data: null };

    // XXX: shouldn't happen
    if (!this.stylish) {
      res.message = 'failed to get stylish component';
      return res;
    }

    var nodes = document.createElement('nodes');

    var list = this.stylish.list(0, {});

    if (list)
      list.forEach(function(s) {
          var e = document.createElement('style');
          e.setAttribute('id', s.id);
          e.setAttribute('url', s.url);
          e.setAttribute('updateUrl', s.updateUrl);
          e.setAttribute('md5Url', s.md5Url);
          e.setAttribute('name', s.name);
          e.setAttribute('enabled', s.enabled);

          var c = document.createElement('code');
          c.appendChild(document.createTextNode(s.code));
          e.appendChild(c);

          if (s.originalCode) {
            var c = document.createElement('originalCode');
            c.appendChild(document.createTextNode(s.originalCode));
            e.appendChild(c);
          }

          nodes.appendChild(e);
        });

    if (nodes.childNodes.length) {
      res.status = true;
      res.data = new XMLSerializer().serializeToString(nodes);
    } else
      res.message = 'no styles found';

    return res;
  }
};

dataSync.registerHandler(dataSync_bookmarks);
dataSync.registerHandler(dataSync_cookies);
dataSync.registerHandler(dataSync_permissions);
dataSync.registerHandler(dataSync_stylish);
