Add jsm file for API. Completely untested, but parses!
authorShawn Wilsher <me@shawnwilsher.com>
Thu, 29 Oct 2009 17:28:31 -0700
changeset 21 7aa4baf393ffe37c7672e93e507e94ddaef38a82
parent 20 13710d2bbd454ea31f5b81546e21a8c387297f9f
child 22 6b3e075d0bd837c10ca84e271e80ae8e85ce1ccb
push id13
push usersdwilsh@shawnwilsher.com
push dateFri, 30 Oct 2009 23:05:59 +0000
Add jsm file for API. Completely untested, but parses!
chrome.manifest
content/reply.js
resource/BugzillaAPI.jsm
--- a/chrome.manifest
+++ b/chrome.manifest
@@ -1,2 +1,3 @@
 overlay chrome://messenger/content/messenger.xul chrome://bugzilla-helper/content/overlay.xul
 content bugzilla-helper content/
+resource bugzilla-helper resource/
--- a/content/reply.js
+++ b/content/reply.js
@@ -1,21 +1,28 @@
+Components.utils.import("resource://bugzilla-helper/BugzillaAPI.jsm");
+
 ////////////////////////////////////////////////////////////////////////////////
 //// Constants
 
 // DEBUG constants
 /*
-const kDestinationURI = "https://api-dev.bugzilla.mozilla.org/stage/0.1/";
+const kServer = new BugzillaServer(
+  "https://api-dev.bugzilla.mozilla.org/stage/0.1/",
+  "75:B2:EF:89:81:51:8F:99:13:DC:BF:44:47:0E:A9:8D"
+);
 const kBugzillaURI = "https://bugzilla-stage-tip.mozilla.org/";
 */
 
-const kDestinationURI = "https://api-dev.bugzilla.mozilla.org/0.1/";
+const kServer = new BugzillaServer(
+  "https://api-dev.bugzilla.mozilla.org/0.1/",
+  "75:B2:EF:89:81:51:8F:99:13:DC:BF:44:47:0E:A9:8D"
+);
+
 const kBugzillaURI = "https://bugzilla.mozilla.org/";
-
-const kFingerprint = "75:B2:EF:89:81:51:8F:99:13:DC:BF:44:47:0E:A9:8D";
 const kBugzillaRealm = "bugzilla-helper";
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Dialog Controller
 
 var ReplyDialog = {
   initialize: function RD_initialize()
   {
@@ -27,49 +34,34 @@ var ReplyDialog = {
       lines.push("> " + line);
     });
     comment.value = lines.join("\n") + "\n";
     comment.focus();
   },
 
   onAccept: function RD_onAccept()
   {
-    // TODO handle offline!
+    let bugID = window.arguments[0];
     let [username, password, store] = this.getLoginInformation();
-    const auth = "username=" + username + "&password=" + password;
-    let bugID = window.arguments[0];
-    const URI = kDestinationURI + "bug/" + bugID + "/comment?" + auth;
-    let req = new UntrustedHttpsRequest(kFingerprint);
-    // XXX make async
-    req.open("POST", URI, false);
-    req.overrideMimeType("application/json");
-    req.setRequestHeader("Accept", "application/json");
-    req.send(ReplyDialog.createJSON());
-    if (req.status == 201) {
-      // HTTP/201 Created
-      if (store)
-        this.storePassword(username, password);
-      return true;
-    }
+    // XXX handle incorrect auth
+    let auth = new BugzillaAuth(username, password);
+    let callback = function(aResponseCode, aResponse, aComment, aIsPrivate) {
+      if (aResponseCode == 200) {
+        if (store)
+          ReplyDialog.storePassword(username, password);
+        return;
+      }
+      // We had something unexpected.  Tell the user.
+      alert("Got HTTP/" + aResponseCode + "\n" + aResponse + "\n for comment" +
+            aComment);
+    };
 
-    // TODO error handling!
-    alert("Sorry, but an error occurred!\n" + req.status + "\n" +
-          req.responseText);
-    return false;
-  },
-
-  createJSON: function RD_createJSON()
-  {
-    let comment = document.getElementById("comment").value;
-    let private = document.getElementById("private").checked;
-    let data = {
-      is_private: private ? 1 : 0,
-      text: comment,
-    };
-    return JSON.stringify(data);
+    BugzillaAPI.addComment(kServer, bugID, auth, comment, isPrivate, callback);
+    // TODO add to activity manager
+    return true;
   },
 
   getLoginInformation: function RD_getLoginInformation()
   {
     // NOTE this isn't my actually password.  I changed it on the staging server
     // to be something weak.
     let shouldStore = false;
     let [username, password] = this.getStoredPassword();
new file mode 100644
--- /dev/null
+++ b/resource/BugzillaAPI.jsm
@@ -0,0 +1,316 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 et filetype=javascript
+ * ***** 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 Bugzilla REST API JS module code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (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 ***** */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+let EXPORTED_SYMBOLS = [
+  "BugzillaAPI",
+  "BugzillaServer",
+  "BugzillaAuth",
+];
+
+////////////////////////////////////////////////////////////////////////////////
+//// Constants
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+///////////////////////////////////////////////////////////////////////////////
+//// Service Constructors
+
+let XMLHttpRequest = Components.constructor(
+  "@mozilla.org/xmlextras/xmlhttprequest;1",
+  "nsIXMLHttpRequest"
+);
+
+////////////////////////////////////////////////////////////////////////////////
+//// BugzillaAPI
+
+const BugzillaAPI = {
+  /**
+   * Adds a comment to the specified bug.
+   *
+   * @param aServer
+   *        The BugzillaServer to connect to.
+   * @param aBugNumber
+   *        The bug number to add a comment for.
+   * @param aAuth
+   *        The BugzillaAuth to use to authenticate.
+   * @param aComment
+   *        The text of the comment to add to aBugNumber.
+   * @param aIsPrivate
+   *        Indicates if this comment should be marked as private or not.
+   * @param aCallback
+   *        The function to call on error or completion with the following
+   *        arguments:
+   *        aResponseCode - the HTTP response code from the request
+   *        aResponse     - the body of the HTTP response
+   *        aComment      - the comment that was sent
+   *        aIsPrivate    - indicating if the comment was private or not
+   */
+  addComment: function BAPI_addComment(aServer,
+                                       aBugNumber,
+                                       aAuth,
+                                       aComment,
+                                       aIsPrivate,
+                                       aCallback)
+  {
+    if (!(aServer instanceof BugzillaServer)) {
+      throw new Components.Exception(
+        "Server must be a BugzillaServer object",
+        Cr.NS_ERROR_INVALID_ARG,
+        Components.stack.caller
+      );
+    }
+    if (!(aAuth instanceof BugzillaAuth)) {
+      throw new Components.Exception(
+        "Authentication must be a BugzillaAuth object",
+        Cr.NS_ERROR_INVALID_ARG,
+        Components.stack.caller
+      );
+    }
+
+    let uri = aServer.uri + "bug/" + aBugNumber + "/comment?" +
+              aAuth.getAuthString();
+    let data = JSON.stringify({
+      is_private: aIsPrivate ? 1 : 0,
+      text: aComment,
+    });
+    let callback = function(aResponseCode, aResponse) {
+      aCallback(aResponseCode, aResponse, aComment, aIsPrivate);
+    };
+
+    performRequest(aServer, uri, callback, data);
+  },
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// BugzillaServer
+
+/**
+ * Constructor for a server object that represents a bugzilla server.
+ *
+ * @param aURI
+ *        The URI of the server.
+ * @param aFingerprint
+ *        The MD5 fingerprint of the certificate encrypting the connection.
+ */
+function BugzillaServer(aURI,
+                        aFingerprint)
+{
+  if (!aURI || !aFingerprint) {
+    throw new Components.Exception(
+      "Must provide a server and a fingerprint",
+      Cr.NS_ERROR_INVALID_ARG,
+      Components.stack.caller
+    );
+  }
+  this.uri = aURI;
+  this.fingerprint = aFingerprint;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// BugzillaAuth
+
+/**
+ * Constructor for an authentication object that stores a username and password.
+ *
+ * @param aUsername
+ *        The username to authenticate with.
+ * @param aPassword
+ *        The password to authenticate with.
+ */
+function BugzillaAuth(aUsername,
+                      aPassword)
+{
+  if (!aUsername || !aPassword) {
+    throw new Components.Exception(
+      "Must provide a username and a password",
+      Cr.NS_ERROR_INVALID_ARG,
+      Components.stack.caller
+    );
+  }
+  this.username = aUsername;
+  this.password = aPassword;
+}
+
+BugzillaAuth.prototype = {
+  get authString()
+  {
+    return "username=" + this.username + "&password=" + this.password;
+  },
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// UntrustedHttpsRequest
+
+/**
+ * Wraps an XMLHttpRequest to make a safe and secure connection to an untrusted
+ * https server.  Consumers MUST provide an MD5 fingerprint of the server the
+ * connection is going to for this to be safe.
+ *
+ * This object can then be used just like an XMLHttpRequest object.
+ *
+ * @param aFingerprint
+ *        The MD5 fingerprint of the certificate encrypting the connection.
+ */
+function UntrustedHttpsRequest(aFingerprint)
+{
+  if (!aFingerprint) {
+    throw new Components.Exception("Must provide an MD5 fingerprint!",
+                                   Cr.NS_ERROR_INVALID_ARG,
+                                   Components.stack.caller);
+  }
+  let req = new XMLHttpRequest();
+  this.__proto__.__proto__ = req;
+  this._wrappedOpen = req.open;
+  this._callback._fingerprint = aFingerprint;
+}
+
+UntrustedHttpsRequest.prototype = {
+  //////////////////////////////////////////////////////////////////////////////
+  //// UntrustedHttpsRequest
+
+  _callback: {
+    ////////////////////////////////////////////////////////////////////////////
+    //// nsIBadCertListener2
+
+    notifyCertProblem: function UHR_notifyCertProblem(aSocketInfo,
+                                                      aStatus,
+                                                      aTargetSite)
+    {
+      let certificate = aStatus.serverCert;
+      // Check to ensure that our fingerprint is what we expect.
+      if (certificate.md5Fingerprint != this._fingerprint)
+        return false;
+
+      // aTargetSite is a host:port combination.
+      let host, port;
+      let (items = aTargetSite.split(":")) {
+        host = items[0];
+        port = items[1];
+      };
+
+      // And add the exception.
+      const ERROR_UNTRUSTED = Ci.nsICertOverrideService.ERROR_UNTRUSTED;
+      let cow = Cc["@mozilla.org/security/certoverride;1"]
+                .getService(Ci.nsICertOverrideService);
+      cow.rememberValidityOverride(host, port, certificate, ERROR_UNTRUSTED,
+                                   true);
+      return true;
+    },
+
+    ////////////////////////////////////////////////////////////////////////////
+    //// nsIInterfaceRequestor
+
+    getInterface: function UHR_getInterace(aIID)
+    {
+      return this.QueryInterface(aIID);
+    },
+
+    ////////////////////////////////////////////////////////////////////////////
+    //// nsISupports
+
+    QueryInterface: XPCOMUtils.generateQI([
+      Ci.nsIBadCertListener2,
+      Ci.nsIIntefaceRequestor,
+    ]),
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// XMLHttpRequest
+
+  open: function UHR_open(aMethod, aURI)
+  {
+    // We want to make sure that the certificate will be added, so we open up
+    // another request now to the address.
+    let req = new XMLHttpRequest();
+    req.open("HEAD", aURI, false);
+    req.channel.notificationCallbacks = this._callback;
+    try {
+      req.send(null);
+    }
+    catch (e) {
+      // Unconditionally catch the exception about the load failing.
+    }
+    this._wrappedOpen.apply(this, arguments);
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsISupports
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIXMLHttpRequest,
+  ]),
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// Global Functions
+
+/**
+ * @param aServer
+ *        The server to connect to.
+ * @param aURI
+ *        The URI to connect to on aServer.
+ * @param aCallback
+ *        The function to call on error or completion with the following
+ *        arguments:
+ *        aResponseCode - the HTTP response code from the request
+ *        aResponse     - the body of the HTTP response
+ * @param aData [optional]
+ *        The data to send in the request.
+ */
+function performRequest(aServer,
+                        aURI,
+                        aCallback,
+                        aData)
+{
+  // TODO handle offline
+  let req = new UntrustedHttpsRequest(aServer.fingerprint);
+  let type = aData ? "POST" : "GET";
+  req.open(type, aURI, true);
+  req.overrideMimeType("application/json");
+  req.setRequestHeader("Accept", "application/json");
+  req.onreadystatechange = function(aEvent) {
+    if (req.readyState == 4)
+      aCallback(req.status, req.responseText);
+  };
+  req.send(aData);
+}