Bug 779117 - Import XMPP fixes from Instantbird - Bio 1556 - Download the user's vCard before uploading an avatar, r=clokep, a=Standard8.
authorFlorian Quèze <florian@instantbird.org>
Thu, 26 Jul 2012 19:35:09 +0200
changeset 12518 b700ec636fd21d9b9d9eb901cf29ab626bfca66a
parent 12517 37202ee972f61e4059d9d0740abf92b2678cefb9
child 12519 521da2a5b2d7f50781a3dc09df57d4f54853aae4
push idunknown
push userunknown
push dateunknown
reviewersclokep, Standard8
bugs779117
Bug 779117 - Import XMPP fixes from Instantbird - Bio 1556 - Download the user's vCard before uploading an avatar, r=clokep, a=Standard8.
chat/protocols/xmpp/xmpp.jsm
--- a/chat/protocols/xmpp/xmpp.jsm
+++ b/chat/protocols/xmpp/xmpp.jsm
@@ -584,19 +584,21 @@ const XMPPAccountPrototype = {
         if ("_shouldSendPresenceForIdlenessChange" in this)
           this._sendPresence();
       }).bind(this));
     }
     else if (aTopic == "status-changed")
       this._sendPresence();
     else if (aTopic == "user-icon-changed") {
       delete this._cachedUserIcon;
+      this._forceUserIconUpdate = true;
       this._sendVCard();
     }
     else if (aTopic == "user-display-name-changed")
+      this._forceUserDisplayNameUpdate = true;
       this._sendVCard();
   },
 
   /* GenericAccountPrototype events */
   /* Connect to the server */
   connect: function() {
     this._jid = this._parseJID(this.name);
 
@@ -1040,16 +1042,19 @@ const XMPPAccountPrototype = {
 
     this._connection.disconnect();
     delete this._connection;
 
     // We won't receive "user-icon-changed" notifications while the
     // account isn't connected, so clear the cache to avoid keeping an
     // obsolete icon.
     delete this._cachedUserIcon;
+    // Also clear the cached user vCard, as we will want to redownload it
+    // after reconnecting.
+    delete this._userVCard;
 
     this.reportDisconnected();
   },
 
   /* Set the user status on the server */
   _sendPresence: function() {
     delete this._shouldSendPresenceForIdlenessChange;
 
@@ -1072,18 +1077,50 @@ const XMPPAccountPrototype = {
       children.push(Stanza.node("status", null, null, statusText));
     if (this._idleSince) {
       let time = Math.floor(Date.now() / 1000) - this._idleSince;
       children.push(Stanza.node("query", Stanza.NS.last, {seconds: time}));
     }
     this._connection.sendStanza(Stanza.presence({"xml:lang": "en"}, children));
   },
 
+  _downloadingUserVCard: false,
+  _downloadUserVCard: function() {
+    // If a download is already in progress, don't start another one.
+    if (this._downloadingUserVCard)
+      return;
+    this._downloadingUserVCard = true;
+    let s = Stanza.iq("get", null, null,
+                      Stanza.node("vCard", Stanza.NS.vcard));
+    this._connection.sendStanza(s, this.onUserVCard, this);
+  },
+
+  onUserVCard: function(aStanza) {
+    delete this._downloadingUserVCard;
+    this._userVCard = aStanza.getElement(["vCard"]) || null;
+    // If a user icon exists in the vCard we received from the server,
+    // we need to ensure the line breaks in its binval are exactly the
+    // same as those we would include if we sent the icon, and that
+    // there isn't any other whitespace.
+    if (this._userVCard) {
+      let binval = this._userVCard.getElement(["PHOTO", "BINVAL"]);
+      if (binval && binval.children.length) {
+        binval = binval.children[0];
+        binval.text = binval.text.replace(/[^A-Za-z0-9\+\/\=]/g, "")
+                                 .replace(/.{74}/g, "$&\n");
+      }
+    }
+    this._sendVCard();
+  },
+
   _cachingUserIcon: false,
   _cacheUserIcon: function() {
+    if (this._cachingUserIcon)
+      return;
+
     let userIcon = this.imAccount.statusInfo.getUserIcon();
     if (!userIcon) {
       this._cachedUserIcon = null;
       this._sendVCard();
       return;
     }
 
     this._cachingUserIcon = true;
@@ -1121,32 +1158,85 @@ const XMPPAccountPrototype = {
       delete this._cachingUserIcon;
       this._sendVCard();
     }).bind(this));
   },
   _sendVCard: function() {
     if (!this._connection)
       return;
 
+    // We have to download the user's existing vCard before updating it.
+    // This lets us preserve the fields that we don't change or don't know.
+    // Some servers may reject a new vCard if we don't do this first.
+    if (!this.hasOwnProperty("_userVCard")) {
+      // The download of the vCard is asyncronous and will call _sendVCard back
+      // when the user's vCard has been received.
+      this._downloadUserVCard();
+      return;
+    }
+
+    // Read the local user icon asynchronously from the disk.
+    // _cacheUserIcon will call _sendVCard back once the icon is ready.
     if (!this.hasOwnProperty("_cachedUserIcon")) {
-      if (!this._cachingUserIcon)
-        this._cacheUserIcon();
+      this._cacheUserIcon();
       return;
     }
 
-    let vCardEntries = [];
+    // If the user currently doesn't have any vCard on the server or
+    // the download failed, an empty new one.
+    if (!this._userVCard)
+      this._userVCard = Stanza.node("vCard", Stanza.NS.vcard);
+
+    // Keep a serialized copy of the existing user vCard so that we
+    // can avoid resending identical data to the server.
+    let existingVCard = this._userVCard.getXML();
+
+    let fn = this._userVCard.getElement(["FN"]);
     let displayName = this.imAccount.statusInfo.displayName;
-    if (displayName)
-      vCardEntries.push(Stanza.node("FN", Stanza.NS.vcard, null, displayName));
+    if (displayName) {
+      // If a display name is set locally, update or add an FN field to the vCard.
+      if (!fn)
+        this._userVCard.addChild(Stanza.node("FN", Stanza.NS.vcard, null, displayName));
+      else {
+        if (fn.children.length)
+          fn.children[0].text = displayName;
+        else
+          fn.addText(displayName);
+      }
+    }
+    else if ("_forceUserDisplayNameUpdate" in this) {
+      // We remove a display name stored on the server without replacing
+      // it with a new value only if this _sendVCard call is the result of
+      // a user action. This is to avoid removing data from the server each
+      // time the user connects from a new profile.
+      this._userVCard.children =
+        this._userVCard.children.filter(function (n) n.qName != "FN");
+    }
+    delete this._forceUserDisplayNameUpdate;
+
     if (this._cachedUserIcon) {
+      // If we have a local user icon, update or add it in the PHOTO field.
       let photoChildren = [
         Stanza.node("TYPE", Stanza.NS.vcard, null, this._cachedUserIcon.type),
         Stanza.node("BINVAL", Stanza.NS.vcard, null, this._cachedUserIcon.binval)
       ];
-      vCardEntries.push(Stanza.node("PHOTO", Stanza.NS.vcard, null,
-                                    photoChildren));
+      let photo = this._userVCard.getElement(["PHOTO"]);
+      if (photo)
+        photo.children = photoChildren;
+      else
+        this._userVCard.addChild(Stanza.node("PHOTO", Stanza.NS.vcard, null,
+                                             photoChildren));
     }
-    let s = Stanza.iq("set", null, null,
-                      Stanza.node("vCard", Stanza.NS.vcard, null, vCardEntries));
+    else if ("_forceUserIconUpdate" in this) {
+      // Like for the display name, we remove a photo without
+      // replacing it only if the call is caused by a user action.
+      this._userVCard.children =
+        this._userVCard.children.filter(function (n) n.qName != "PHOTO");
+    }
+    delete this._forceUserIconUpdate;
 
-    this._connection.sendStanza(s);
+    // Send the vCard only if it has really changed.
+    if (this._userVCard.getXML() != existingVCard)
+      this._connection.sendStanza(Stanza.iq("set", null, null, this._userVCard));
+    else
+      LOG("Not sending the vCard because the server stored vCard is identical.");
   }
 };