1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5const EXPORTED_SYMBOLS = [
6  "XMPPConversationPrototype",
7  "XMPPMUCConversationPrototype",
8  "XMPPAccountBuddyPrototype",
9  "XMPPAccountPrototype",
10];
11
12const { clearTimeout, setTimeout } = ChromeUtils.import(
13  "resource://gre/modules/Timer.jsm"
14);
15const { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
16const { Status } = ChromeUtils.import("resource:///modules/imStatusUtils.jsm");
17const {
18  XPCOMUtils,
19  executeSoon,
20  nsSimpleEnumerator,
21  EmptyEnumerator,
22  ClassInfo,
23  l10nHelper,
24} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
25var {
26  GenericAccountPrototype,
27  GenericAccountBuddyPrototype,
28  GenericConvIMPrototype,
29  GenericConvChatPrototype,
30  GenericConversationPrototype,
31  TooltipInfo,
32} = ChromeUtils.import("resource:///modules/jsProtoHelper.jsm");
33const { NormalizedMap } = ChromeUtils.import(
34  "resource:///modules/NormalizedMap.jsm"
35);
36var { Stanza, SupportedFeatures } = ChromeUtils.import(
37  "resource:///modules/xmpp-xml.jsm"
38);
39var { XMPPSession } = ChromeUtils.import(
40  "resource:///modules/xmpp-session.jsm"
41);
42
43ChromeUtils.defineModuleGetter(
44  this,
45  "DownloadUtils",
46  "resource://gre/modules/DownloadUtils.jsm"
47);
48ChromeUtils.defineModuleGetter(
49  this,
50  "FileUtils",
51  "resource://gre/modules/FileUtils.jsm"
52);
53ChromeUtils.defineModuleGetter(
54  this,
55  "NetUtil",
56  "resource://gre/modules/NetUtil.jsm"
57);
58XPCOMUtils.defineLazyServiceGetter(
59  this,
60  "imgTools",
61  "@mozilla.org/image/tools;1",
62  "imgITools"
63);
64XPCOMUtils.defineLazyServiceGetter(
65  this,
66  "UuidGenerator",
67  "@mozilla.org/uuid-generator;1",
68  "nsIUUIDGenerator"
69);
70
71XPCOMUtils.defineLazyGetter(this, "_", () =>
72  l10nHelper("chrome://chat/locale/xmpp.properties")
73);
74
75XPCOMUtils.defineLazyGetter(this, "TXTToHTML", function() {
76  let cs = Cc["@mozilla.org/txttohtmlconv;1"].getService(Ci.mozITXTToHTMLConv);
77  return aTxt => cs.scanTXT(aTxt, cs.kEntities);
78});
79
80// Parses the status from a presence stanza into an object of statusType,
81// statusText and idleSince.
82function parseStatus(aStanza) {
83  let statusType = Ci.imIStatusInfo.STATUS_AVAILABLE;
84  let show = aStanza.getElement(["show"]);
85  if (show) {
86    show = show.innerText;
87    if (show == "away") {
88      statusType = Ci.imIStatusInfo.STATUS_AWAY;
89    } else if (show == "chat") {
90      statusType = Ci.imIStatusInfo.STATUS_AVAILABLE; // FIXME
91    } else if (show == "dnd") {
92      statusType = Ci.imIStatusInfo.STATUS_UNAVAILABLE;
93    } else if (show == "xa") {
94      statusType = Ci.imIStatusInfo.STATUS_IDLE;
95    }
96  }
97
98  let idleSince = 0;
99  let date = _getDelay(aStanza);
100  if (date) {
101    idleSince = date.getTime();
102  }
103
104  let query = aStanza.getElement(["query"]);
105  if (query && query.uri == Stanza.NS.last) {
106    let now = Math.floor(Date.now() / 1000);
107    idleSince = now - parseInt(query.attributes.seconds, 10);
108    statusType = Ci.imIStatusInfo.STATUS_IDLE;
109  }
110
111  // Mark official Android clients as mobile.
112  const kAndroidNodeURI = "http://www.android.com/gtalk/client/caps";
113  if (
114    aStanza
115      .getChildrenByNS(Stanza.NS.caps)
116      .some(s => s.localName == "c" && s.attributes.node == kAndroidNodeURI)
117  ) {
118    statusType = Ci.imIStatusInfo.STATUS_MOBILE;
119  }
120
121  let status = aStanza.getElement(["status"]);
122  status = status ? status.innerText : "";
123
124  return { statusType, statusText: status, idleSince };
125}
126
127// Returns a Date object for the delay value (stamp) in aStanza if it exists,
128// otherwise returns undefined.
129function _getDelay(aStanza) {
130  // XEP-0203: Delayed Delivery.
131  let date;
132  let delay = aStanza.getElement(["delay"]);
133  if (delay && delay.uri == Stanza.NS.delay) {
134    if (delay.attributes.stamp) {
135      date = new Date(delay.attributes.stamp);
136    }
137  }
138  if (date && isNaN(date.getTime())) {
139    return undefined;
140  }
141
142  return date;
143}
144
145// Writes aMsg in aConv as an outgoing message with optional date as the
146// message may be sent from another client.
147function _displaySentMsg(aConv, aMsg, aDate) {
148  let who;
149  if (aConv._account._connection) {
150    who = aConv._account._connection._jid.jid;
151  }
152  if (!who) {
153    who = aConv._account.name;
154  }
155
156  let flags = { outgoing: true };
157  flags._alias = aConv.account.alias || aConv.account.statusInfo.displayName;
158
159  if (aDate) {
160    flags.time = aDate / 1000;
161    flags.delayed = true;
162  }
163  aConv.writeMessage(who, aMsg, flags);
164}
165
166// The timespan after which we consider roomInfo to be stale.
167var kListRefreshInterval = 12 * 60 * 60 * 1000; // 12 hours.
168
169/* This is an ordered list, used to determine chat buddy flags:
170 *  index = member    -> voiced
171 *          moderator -> moderator
172 *          admin     -> admin
173 *          owner     -> founder
174 */
175var kRoles = [
176  "outcast",
177  "visitor",
178  "participant",
179  "member",
180  "moderator",
181  "admin",
182  "owner",
183];
184
185function MUCParticipant(aNick, aJid, aPresenceStanza) {
186  this._jid = aJid;
187  this.name = aNick;
188  this.onPresenceStanza(aPresenceStanza);
189}
190MUCParticipant.prototype = {
191  __proto__: ClassInfo("prplIConvChatBuddy", "XMPP ConvChatBuddy object"),
192
193  buddy: false,
194
195  // The occupant jid of the participant which is of the form room@domain/nick.
196  _jid: null,
197
198  // The real jid of the participant which is of the form local@domain/resource.
199  accountJid: null,
200
201  statusType: null,
202  statusText: null,
203  get alias() {
204    return this.name;
205  },
206
207  role: 2, // "participant" by default
208
209  // Called when a presence stanza is received for this participant.
210  onPresenceStanza(aStanza) {
211    let statusInfo = parseStatus(aStanza);
212    this.statusType = statusInfo.statusType;
213    this.statusText = statusInfo.statusText;
214
215    let x = aStanza.children.filter(
216      child => child.localName == "x" && child.uri == Stanza.NS.muc_user
217    );
218    if (x.length == 0) {
219      return;
220    }
221
222    // XEP-0045 (7.2.3): We only expect a single <x/> element of this namespace,
223    // so we ignore any others.
224    x = x[0];
225
226    let item = x.getElement(["item"]);
227    if (!item) {
228      return;
229    }
230
231    this.role = Math.max(
232      kRoles.indexOf(item.attributes.role),
233      kRoles.indexOf(item.attributes.affiliation)
234    );
235
236    let accountJid = item.attributes.jid;
237    if (accountJid) {
238      this.accountJid = accountJid;
239    }
240  },
241
242  get voiced() {
243    /* FIXME: The "voiced" role corresponds to users that can send messages to
244     * the room. If the chat is unmoderated, this should include everyone, not
245     * just members. */
246    return this.role == kRoles.indexOf("member");
247  },
248  get moderator() {
249    return this.role == kRoles.indexOf("moderator");
250  },
251  get admin() {
252    return this.role == kRoles.indexOf("admin");
253  },
254  get founder() {
255    return this.role == kRoles.indexOf("owner");
256  },
257  typing: false,
258};
259
260// MUC (Multi-User Chat)
261var XMPPMUCConversationPrototype = {
262  __proto__: GenericConvChatPrototype,
263  // By default users are not in a MUC.
264  _left: true,
265
266  // Tracks all received messages to avoid possible duplication if the server
267  // sends us the last few messages again when we rejoin a room.
268  _messageIds: new Set(),
269
270  _init(aAccount, aJID, aNick) {
271    this._messageIds = new Set();
272    GenericConvChatPrototype._init.call(this, aAccount, aJID, aNick);
273  },
274
275  _targetResource: "",
276
277  // True while we are rejoining a room previously parted by the user.
278  _rejoined: false,
279
280  get topic() {
281    return this._topic;
282  },
283  set topic(aTopic) {
284    // XEP-0045 (8.1): Modifying the room subject.
285    let subject = Stanza.node("subject", null, null, aTopic.trim());
286    let s = Stanza.message(
287      this.name,
288      null,
289      null,
290      { type: "groupchat" },
291      subject
292    );
293    let notAuthorized = _("conversation.error.changeTopicFailedNotAuthorized");
294    this._account.sendStanza(
295      s,
296      this._account.handleErrors(
297        {
298          forbidden: notAuthorized,
299          notAcceptable: notAuthorized,
300          itemNotFound: notAuthorized,
301        },
302        this
303      )
304    );
305  },
306  get topicSettable() {
307    return true;
308  },
309
310  /* Called when the user enters a chat message */
311  sendMsg(aMsg) {
312    // XEP-0045 (7.4): Sending a message to all occupants in a room.
313    let s = Stanza.message(this.name, aMsg, null, { type: "groupchat" });
314    let notInRoom = _(
315      "conversation.error.sendFailedAsNotInRoom",
316      this.name,
317      aMsg
318    );
319    this._account.sendStanza(
320      s,
321      this._account.handleErrors(
322        {
323          itemNotFound: notInRoom,
324          notAcceptable: notInRoom,
325        },
326        this
327      )
328    );
329  },
330
331  /* Called by the account when a presence stanza is received for this muc */
332  onPresenceStanza(aStanza) {
333    let from = aStanza.attributes.from;
334    let nick = this._account._parseJID(from).resource;
335    let jid = this._account.normalize(from);
336    let x = aStanza.getElements(["x"]).find(e => e.uri == Stanza.NS.muc_user);
337
338    // Check if the join failed.
339    if (this.left && aStanza.attributes.type == "error") {
340      let error = this._account.parseError(aStanza);
341      let message;
342      switch (error.condition) {
343        case "not-authorized":
344        case "registration-required":
345          // XEP-0045 (7.2.7): Members-Only Rooms.
346          message = _("conversation.error.joinFailedNotAuthorized");
347          break;
348        case "not-allowed":
349          message = _("conversation.error.creationFailedNotAllowed");
350          break;
351        case "remote-server-not-found":
352          message = _(
353            "conversation.error.joinFailedRemoteServerNotFound",
354            this.name
355          );
356          break;
357        case "forbidden":
358          // XEP-0045 (7.2.8): Banned users.
359          message = _("conversation.error.joinForbidden", this.name);
360          break;
361        default:
362          message = _("conversation.error.joinFailed", this.name);
363          this.ERROR("Failed to join MUC: " + aStanza.convertToString());
364          break;
365      }
366      this.writeMessage(this.name, message, { system: true, error: true });
367      this.joining = false;
368      return;
369    }
370
371    if (!x) {
372      this.WARN(
373        "Received a MUC presence stanza without an x element or " +
374          "with a namespace we don't handle."
375      );
376      return;
377    }
378    let codes = x.getElements(["status"]).map(elt => elt.attributes.code);
379    let item = x.getElement(["item"]);
380
381    // Changes the nickname of a participant for this muc.
382    let changeNick = () => {
383      if (!item || !item.attributes.nick) {
384        this.WARN(
385          "Received a MUC presence code 303 or 210 stanza without an " +
386            "item element or a nick attribute."
387        );
388        return;
389      }
390      let newNick = item.attributes.nick;
391      this.updateNick(nick, newNick, nick == this.nick);
392    };
393
394    if (aStanza.attributes.type == "unavailable") {
395      if (!this._participants.has(nick)) {
396        this.WARN(
397          "received unavailable presence for an unknown MUC participant: " +
398            from
399        );
400        return;
401      }
402      if (codes.includes("303")) {
403        // XEP-0045 (7.6): Changing Nickname.
404        // Service Updates Nick for user.
405        changeNick();
406        return;
407      }
408      if (item && item.attributes.role == "none") {
409        // XEP-0045: an occupant has left the room.
410        this.removeParticipant(nick);
411
412        // Who caused the participant to leave the room.
413        let actor = item.getElement(["actor"]);
414        let actorNick = actor ? actor.attributes.nick : "";
415        let isActor = actorNick ? ".actor" : "";
416
417        // Why the participant left.
418        let reasonNode = item.getElement(["reason"]);
419        let reason = reasonNode ? reasonNode.innerText : "";
420        let isReason = reason ? ".reason" : "";
421
422        let isYou = nick == this.nick ? ".you" : "";
423        let affectedNick = isYou ? "" : nick;
424        if (isYou) {
425          this.left = true;
426        }
427
428        let message;
429        if (codes.includes("301")) {
430          // XEP-0045 (9.1): Banning a User.
431          message = "conversation.message.banned";
432        } else if (codes.includes("307")) {
433          // XEP-0045 (8.2): Kicking an Occupant.
434          message = "conversation.message.kicked";
435        } else if (codes.includes("322") || codes.includes("321")) {
436          // XEP-0045: Inform user that he or she is being removed from the
437          // room because the room has been changed to members-only and the
438          // user is not a member.
439          message = "conversation.message.removedNonMember";
440        } else if (codes.includes("332")) {
441          // XEP-0045: Inform user that he or she is being removed from the
442          // room because the MUC service is being shut down.
443          message = "conversation.message.mucShutdown";
444
445          // The reason here just duplicates what's in the system message.
446          reason = isReason = "";
447        } else {
448          // XEP-0045 (7.14): Received when the user parts a room.
449          message = "conversation.message.parted";
450
451          // The reason is in a status element in this case.
452          reasonNode = aStanza.getElement(["status"]);
453          reason = reasonNode ? reasonNode.innerText : "";
454          isReason = reason ? ".reason" : "";
455        }
456
457        if (message) {
458          let messageID = message + isYou + isActor + isReason;
459          let params = [actorNick, affectedNick, reason].filter(s => s);
460          this.writeMessage(this.name, _(messageID, ...params), {
461            system: true,
462          });
463        }
464      } else {
465        this.WARN("Unhandled type==unavailable MUC presence stanza.");
466      }
467      return;
468    }
469
470    if (codes.includes("201")) {
471      // XEP-0045 (10.1): Creating room.
472      // Service Acknowledges Room Creation
473      // and Room is awaiting configuration.
474      // XEP-0045 (10.1.2): Instant room.
475      let query = Stanza.node(
476        "query",
477        Stanza.NS.muc_owner,
478        null,
479        Stanza.node("x", Stanza.NS.xdata, { type: "submit" })
480      );
481      let s = Stanza.iq("set", null, jid, query);
482      this._account.sendStanza(s, aStanzaReceived => {
483        if (aStanzaReceived.attributes.type != "result") {
484          return false;
485        }
486
487        // XEP-0045: Service Informs New Room Owner of Success
488        // for instant and reserved rooms.
489        this.left = false;
490        this.joining = false;
491        return true;
492      });
493    } else if (codes.includes("210")) {
494      // XEP-0045 (7.6): Changing Nickname.
495      // Service modifies this user's nickname in accordance with local service
496      // policies.
497      changeNick();
498      return;
499    } else if (codes.includes("110")) {
500      // XEP-0045: Room exists and joined successfully.
501      this.left = false;
502      this.joining = false;
503      // TODO (Bug 1172350): Implement Service Discovery Extensions (XEP-0128) to obtain
504      // configuration of this room.
505    }
506
507    if (!this._participants.get(nick)) {
508      let participant = new MUCParticipant(nick, from, aStanza);
509      this._participants.set(nick, participant);
510      this.notifyObservers(
511        new nsSimpleEnumerator([participant]),
512        "chat-buddy-add"
513      );
514      if (this.nick != nick && !this.joining) {
515        this.writeMessage(this.name, _("conversation.message.join", nick), {
516          system: true,
517        });
518      } else if (this.nick == nick && this._rejoined) {
519        this.writeMessage(this.name, _("conversation.message.rejoined"), {
520          system: true,
521        });
522        this._rejoined = false;
523      }
524    } else {
525      this._participants.get(nick).onPresenceStanza(aStanza);
526      this.notifyObservers(this._participants.get(nick), "chat-buddy-update");
527    }
528  },
529
530  /* Called by the account when a message is received for this muc */
531  incomingMessage(aMsg, aStanza, aDate) {
532    let from = this._account._parseJID(aStanza.attributes.from).resource;
533    let id = aStanza.attributes.id;
534    let flags = {};
535    if (!from) {
536      flags.system = true;
537      from = this.name;
538    } else if (aStanza.attributes.type == "error") {
539      aMsg = _("conversation.error.notDelivered", aMsg);
540      flags.system = true;
541      flags.error = true;
542    } else if (from == this._nick) {
543      flags.outgoing = true;
544    } else {
545      flags.incoming = true;
546    }
547    if (aDate) {
548      flags.time = aDate / 1000;
549      flags.delayed = true;
550    }
551    if (id) {
552      // Checks if a message exists in conversation to avoid duplication.
553      if (this._messageIds.has(id)) {
554        return;
555      }
556      this._messageIds.add(id);
557    }
558    this.writeMessage(from, aMsg, flags);
559  },
560
561  getNormalizedChatBuddyName(aNick) {
562    return this._account.normalizeFullJid(this.name + "/" + aNick);
563  },
564
565  // Leaves MUC conversation.
566  part(aMsg = null) {
567    let s = Stanza.presence(
568      { to: this.name + "/" + this._nick, type: "unavailable" },
569      aMsg ? Stanza.node("status", null, null, aMsg.trim()) : null
570    );
571    this._account.sendStanza(s);
572    delete this.chatRoomFields;
573  },
574
575  // Invites a user to MUC conversation.
576  invite(aJID, aMsg = null) {
577    // XEP-0045 (7.8): Inviting Another User to a Room.
578    // XEP-0045 (7.8.2): Mediated Invitation.
579    let invite = Stanza.node(
580      "invite",
581      null,
582      { to: aJID },
583      aMsg ? Stanza.node("reason", null, null, aMsg) : null
584    );
585    let x = Stanza.node("x", Stanza.NS.muc_user, null, invite);
586    let s = Stanza.node("message", null, { to: this.name }, x);
587    this._account.sendStanza(
588      s,
589      this._account.handleErrors(
590        {
591          forbidden: _("conversation.error.inviteFailedForbidden"),
592          // ejabberd uses error not-allowed to indicate that this account does not
593          // have the required privileges to invite users instead of forbidden error,
594          // and this is not mentioned in the spec (XEP-0045).
595          notAllowed: _("conversation.error.inviteFailedForbidden"),
596          itemNotFound: _("conversation.error.failedJIDNotFound", aJID),
597        },
598        this
599      )
600    );
601  },
602
603  // Bans a participant from MUC conversation.
604  ban(aNickName, aMsg = null) {
605    // XEP-0045 (9.1): Banning a User.
606    let participant = this._participants.get(aNickName);
607    if (!participant) {
608      this.writeMessage(
609        this.name,
610        _("conversation.error.nickNotInRoom", aNickName),
611        { system: true }
612      );
613      return;
614    }
615    if (!participant.accountJid) {
616      this.writeMessage(
617        this.name,
618        _("conversation.error.banCommandAnonymousRoom"),
619        { system: true }
620      );
621      return;
622    }
623
624    let attributes = { affiliation: "outcast", jid: participant.accountJid };
625    let item = Stanza.node(
626      "item",
627      null,
628      attributes,
629      aMsg ? Stanza.node("reason", null, null, aMsg) : null
630    );
631    let s = Stanza.iq(
632      "set",
633      null,
634      this.name,
635      Stanza.node("query", Stanza.NS.muc_admin, null, item)
636    );
637    this._account.sendStanza(s, this._banKickHandler, this);
638  },
639
640  // Kicks a participant from MUC conversation.
641  kick(aNickName, aMsg = null) {
642    // XEP-0045 (8.2): Kicking an Occupant.
643    let attributes = { role: "none", nick: aNickName };
644    let item = Stanza.node(
645      "item",
646      null,
647      attributes,
648      aMsg ? Stanza.node("reason", null, null, aMsg) : null
649    );
650    let s = Stanza.iq(
651      "set",
652      null,
653      this.name,
654      Stanza.node("query", Stanza.NS.muc_admin, null, item)
655    );
656    this._account.sendStanza(s, this._banKickHandler, this);
657  },
658
659  // Callback for ban and kick commands.
660  _banKickHandler(aStanza) {
661    return this._account._handleResult(
662      {
663        notAllowed: _("conversation.error.banKickCommandNotAllowed"),
664        conflict: _("conversation.error.banKickCommandConflict"),
665      },
666      this
667    )(aStanza);
668  },
669
670  // Changes nick in MUC conversation to a new one.
671  setNick(aNewNick) {
672    // XEP-0045 (7.6): Changing Nickname.
673    let s = Stanza.presence({ to: this.name + "/" + aNewNick }, null);
674    this._account.sendStanza(
675      s,
676      this._account.handleErrors(
677        {
678          // XEP-0045 (7.6): Changing Nickname (example 53).
679          // TODO: We should discover if the user has a reserved nickname (maybe
680          // before joining a room), cf. XEP-0045 (7.12).
681          notAcceptable: _(
682            "conversation.error.changeNickFailedNotAcceptable",
683            aNewNick
684          ),
685          // XEP-0045 (7.2.9): Nickname Conflict.
686          conflict: _("conversation.error.changeNickFailedConflict", aNewNick),
687        },
688        this
689      )
690    );
691  },
692
693  // Called by the account when a message stanza is received for this muc and
694  // needs to be handled.
695  onMessageStanza(aStanza) {
696    let x = aStanza.getElement(["x"]);
697    let decline = x.getElement(["decline"]);
698    if (decline) {
699      // XEP-0045 (7.8): Inviting Another User to a Room.
700      // XEP-0045 (7.8.2): Mediated Invitation.
701      let invitee = decline.attributes.jid;
702      let reasonNode = decline.getElement(["reason"]);
703      let reason = reasonNode ? reasonNode.innerText : "";
704      let msg;
705      if (reason) {
706        msg = _(
707          "conversation.message.invitationDeclined.reason",
708          invitee,
709          reason
710        );
711      } else {
712        msg = _("conversation.message.invitationDeclined", invitee);
713      }
714
715      this.writeMessage(this.name, msg, { system: true });
716    } else {
717      this.WARN("Unhandled message stanza.");
718    }
719  },
720
721  /* Called when the user closed the conversation */
722  close() {
723    if (!this.left) {
724      this.part();
725    }
726    GenericConvChatPrototype.close.call(this);
727  },
728  unInit() {
729    this._account.removeConversation(this.name);
730    GenericConvChatPrototype.unInit.call(this);
731  },
732};
733function XMPPMUCConversation(aAccount, aJID, aNick) {
734  this._init(aAccount, aJID, aNick);
735}
736XMPPMUCConversation.prototype = XMPPMUCConversationPrototype;
737
738/* Helper class for buddy conversations */
739var XMPPConversationPrototype = {
740  __proto__: GenericConvIMPrototype,
741
742  _typingTimer: null,
743  supportChatStateNotifications: true,
744  _typingState: "active",
745
746  // Indicates that current conversation is with a MUC participant and the
747  // recipient jid (stored in the userName) is of the form room@domain/nick.
748  _isMucParticipant: false,
749
750  get buddy() {
751    return this._account._buddies.get(this.name);
752  },
753  get title() {
754    return this.contactDisplayName;
755  },
756  get contactDisplayName() {
757    return this.buddy ? this.buddy.contactDisplayName : this.name;
758  },
759  get userName() {
760    return this.buddy ? this.buddy.userName : this.name;
761  },
762
763  // Returns jid (room@domain/nick) if it is with a MUC participant, and the
764  // name of conversation otherwise.
765  get normalizedName() {
766    if (this._isMucParticipant) {
767      return this._account.normalizeFullJid(this.name);
768    }
769    return this._account.normalize(this.name);
770  },
771
772  // Used to avoid showing full jids in typing notifications.
773  get shortName() {
774    if (this.buddy) {
775      return this.buddy.contactDisplayName;
776    }
777
778    let jid = this._account._parseJID(this.name);
779    if (!jid) {
780      return this.name;
781    }
782
783    // Returns nick of the recipient if conversation is with a participant of
784    // a MUC we are in as jid of the recipient is of the form room@domain/nick.
785    if (this._isMucParticipant) {
786      return jid.resource;
787    }
788
789    return jid.node;
790  },
791
792  get shouldSendTypingNotifications() {
793    return (
794      this.supportChatStateNotifications &&
795      Services.prefs.getBoolPref("purple.conversations.im.send_typing")
796    );
797  },
798
799  /* Called when the user is typing a message
800   * aString - the currently typed message
801   * Returns the number of characters that can still be typed */
802  sendTyping(aString) {
803    if (!this.shouldSendTypingNotifications) {
804      return Ci.prplIConversation.NO_TYPING_LIMIT;
805    }
806
807    this._cancelTypingTimer();
808    if (aString.length) {
809      this._typingTimer = setTimeout(this.finishedComposing.bind(this), 10000);
810    }
811
812    this._setTypingState(aString.length ? "composing" : "active");
813
814    return Ci.prplIConversation.NO_TYPING_LIMIT;
815  },
816
817  finishedComposing() {
818    if (!this.shouldSendTypingNotifications) {
819      return;
820    }
821
822    this._setTypingState("paused");
823  },
824
825  _setTypingState(aNewState) {
826    if (this._typingState == aNewState) {
827      return;
828    }
829
830    let s = Stanza.message(this.to, null, aNewState);
831
832    // We don't care about errors in response to typing notifications
833    // (e.g. because the user has left the room when talking to a MUC
834    // participant).
835    this._account.sendStanza(s, () => true);
836
837    this._typingState = aNewState;
838  },
839  _cancelTypingTimer() {
840    if (this._typingTimer) {
841      clearTimeout(this._typingTimer);
842      delete this._typingTimer;
843    }
844  },
845
846  // Holds the resource of user that you are currently talking to, but if the
847  // user is a participant of a MUC we are in, holds the nick of user you are
848  // talking to.
849  _targetResource: "",
850
851  get to() {
852    if (!this._targetResource || this._isMucParticipant) {
853      return this.userName;
854    }
855    return this.userName + "/" + this._targetResource;
856  },
857
858  /* Called when the user enters a chat message */
859  sendMsg(aMsg) {
860    this._cancelTypingTimer();
861    let cs = this.shouldSendTypingNotifications ? "active" : null;
862    let s = Stanza.message(this.to, aMsg, cs);
863    this._account.sendStanza(s);
864    _displaySentMsg(this, aMsg);
865    delete this._typingState;
866  },
867
868  // Invites the contact to a MUC room.
869  invite(aRoomJid, aPassword = null) {
870    // XEP-0045 (7.8): Inviting Another User to a Room.
871    // XEP-0045 (7.8.1) and XEP-0249: Direct Invitation.
872    let x = Stanza.node("x", Stanza.NS.conference, {
873      jid: aRoomJid,
874      password: aPassword,
875    });
876    this._account.sendStanza(Stanza.node("message", null, { to: this.to }, x));
877  },
878
879  // Query the user for its Software Version.
880  // XEP-0092: Software Version.
881  getVersion() {
882    // TODO: Use Service Discovery to determine if the user's client supports
883    // jabber:iq:version protocol.
884
885    let s = Stanza.iq(
886      "get",
887      null,
888      this.to,
889      Stanza.node("query", Stanza.NS.version)
890    );
891    this._account.sendStanza(s, aStanza => {
892      // TODO: handle other errors that can result from querying
893      // user for its software version.
894      if (
895        this._account.handleErrors(
896          {
897            default: _("conversation.error.version.unknown"),
898          },
899          this
900        )(aStanza)
901      ) {
902        return;
903      }
904
905      let query = aStanza.getElement(["query"]);
906      if (!query || query.uri != Stanza.NS.version) {
907        this.WARN(
908          "Received a response to version query which does not " +
909            "contain query element or 'jabber:iq:version' namespace."
910        );
911        return;
912      }
913
914      let name = query.getElement(["name"]);
915      let version = query.getElement(["version"]);
916      if (!name || !version) {
917        // XEP-0092: name and version elements are REQUIRED.
918        this.WARN(
919          "Received a response to version query which does not " +
920            "contain name or version."
921        );
922        return;
923      }
924
925      let messageID = "conversation.message.version";
926      let params = [this.shortName, name.innerText, version.innerText];
927
928      // XEP-0092: os is OPTIONAL.
929      let os = query.getElement(["os"]);
930      if (os) {
931        params.push(os.innerText);
932        messageID += "WithOS";
933      }
934
935      this.writeMessage(this.name, _(messageID, ...params), { system: true });
936    });
937  },
938
939  /* Perform entity escaping before displaying the message. We assume incoming
940     messages have already been escaped, and will otherwise be filtered. */
941  prepareForDisplaying(aMsg) {
942    if (aMsg.outgoing && !aMsg.system) {
943      aMsg.displayMessage = TXTToHTML(aMsg.displayMessage);
944    }
945    GenericConversationPrototype.prepareForDisplaying.apply(this, arguments);
946  },
947
948  /* Called by the account when a message is received from the buddy */
949  incomingMessage(aMsg, aStanza, aDate) {
950    let from = aStanza.attributes.from;
951    this._targetResource = this._account._parseJID(from).resource;
952    let flags = {};
953    let error = this._account.parseError(aStanza);
954    if (error) {
955      let norm = this._account.normalize(from);
956      let muc = this._account._mucs.get(norm);
957
958      if (!aMsg) {
959        // Failed outgoing message.
960        switch (error.condition) {
961          case "remote-server-not-found":
962            aMsg = _("conversation.error.remoteServerNotFound");
963            break;
964          case "service-unavailable":
965            aMsg = _(
966              "conversation.error.sendServiceUnavailable",
967              this.shortName
968            );
969            break;
970          default:
971            aMsg = _("conversation.error.unknownSendError");
972            break;
973        }
974      } else if (
975        this._isMucParticipant &&
976        muc &&
977        !muc.left &&
978        error.condition == "item-not-found"
979      ) {
980        // XEP-0045 (7.5): MUC private messages.
981        // If we try to send to participant not in a room we are in.
982        aMsg = _(
983          "conversation.error.sendFailedAsRecipientNotInRoom",
984          this._targetResource,
985          aMsg
986        );
987      } else if (
988        this._isMucParticipant &&
989        (error.condition == "item-not-found" ||
990          error.condition == "not-acceptable")
991      ) {
992        // If we left a room and try to send to a participant in it or the
993        // room is removed.
994        aMsg = _(
995          "conversation.error.sendFailedAsNotInRoom",
996          this._account.normalize(from),
997          aMsg
998        );
999      } else {
1000        aMsg = _("conversation.error.notDelivered", aMsg);
1001      }
1002      flags.system = true;
1003      flags.error = true;
1004    } else {
1005      flags = { incoming: true, _alias: this.contactDisplayName };
1006    }
1007    if (aDate) {
1008      flags.time = aDate / 1000;
1009      flags.delayed = true;
1010    }
1011    this.writeMessage(from, aMsg, flags);
1012  },
1013
1014  /* Called when the user closed the conversation */
1015  close() {
1016    // TODO send the stanza indicating we have left the conversation?
1017    GenericConvIMPrototype.close.call(this);
1018  },
1019  unInit() {
1020    this._account.removeConversation(this.normalizedName);
1021    GenericConvIMPrototype.unInit.call(this);
1022  },
1023};
1024
1025// Creates XMPP conversation.
1026function XMPPConversation(aAccount, aNormalizedName, aMucParticipant) {
1027  this._init(aAccount, aNormalizedName);
1028  if (aMucParticipant) {
1029    this._isMucParticipant = true;
1030  }
1031}
1032XMPPConversation.prototype = XMPPConversationPrototype;
1033
1034/* Helper class for buddies */
1035var XMPPAccountBuddyPrototype = {
1036  __proto__: GenericAccountBuddyPrototype,
1037
1038  subscription: "none",
1039  // Returns a list of TooltipInfo objects to be displayed when the user
1040  // hovers over the buddy.
1041  getTooltipInfo() {
1042    if (!this._account.connected) {
1043      return null;
1044    }
1045
1046    let tooltipInfo = [];
1047    if (this._resources) {
1048      for (let r in this._resources) {
1049        let status = this._resources[r];
1050        let statusString = Status.toLabel(status.statusType);
1051        if (
1052          status.statusType == Ci.imIStatusInfo.STATUS_IDLE &&
1053          status.idleSince
1054        ) {
1055          let now = Math.floor(Date.now() / 1000);
1056          let valuesAndUnits = DownloadUtils.convertTimeUnits(
1057            now - status.idleSince
1058          );
1059          if (!valuesAndUnits[2]) {
1060            valuesAndUnits.splice(2, 2);
1061          }
1062          statusString += " (" + valuesAndUnits.join(" ") + ")";
1063        }
1064        if (status.statusText) {
1065          statusString += " - " + status.statusText;
1066        }
1067        let label = r ? _("tooltip.status", r) : _("tooltip.statusNoResource");
1068        tooltipInfo.push(new TooltipInfo(label, statusString));
1069      }
1070    }
1071
1072    // The subscription value is interesting to display only in unusual cases.
1073    if (this.subscription != "both") {
1074      tooltipInfo.push(
1075        new TooltipInfo(_("tooltip.subscription"), this.subscription)
1076      );
1077    }
1078
1079    return tooltipInfo;
1080  },
1081
1082  // _rosterAlias is the value stored in the roster on the XMPP
1083  // server. For most servers we will be read/write.
1084  _rosterAlias: "",
1085  set rosterAlias(aNewAlias) {
1086    let old = this.displayName;
1087    this._rosterAlias = aNewAlias;
1088    if (old != this.displayName) {
1089      this._notifyObservers("display-name-changed", old);
1090    }
1091  },
1092  _vCardReceived: false,
1093  // _vCardFormattedName is the display name the contact has set for
1094  // himself in his vCard. It's read-only from our point of view.
1095  _vCardFormattedName: "",
1096  set vCardFormattedName(aNewFormattedName) {
1097    let old = this.displayName;
1098    this._vCardFormattedName = aNewFormattedName;
1099    if (old != this.displayName) {
1100      this._notifyObservers("display-name-changed", old);
1101    }
1102  },
1103
1104  // _serverAlias is set by jsProtoHelper to the value we cached in sqlite.
1105  // Use it only if we have neither of the other two values; usually because
1106  // we haven't connected to the server yet.
1107  get serverAlias() {
1108    return this._rosterAlias || this._vCardFormattedName || this._serverAlias;
1109  },
1110  set serverAlias(aNewAlias) {
1111    if (!this._rosterItem) {
1112      this.ERROR(
1113        "attempting to update the server alias of an account buddy " +
1114          "for which we haven't received a roster item."
1115      );
1116      return;
1117    }
1118
1119    let item = this._rosterItem;
1120    if (aNewAlias) {
1121      item.attributes.name = aNewAlias;
1122    } else if ("name" in item.attributes) {
1123      delete item.attributes.name;
1124    }
1125
1126    let s = Stanza.iq(
1127      "set",
1128      null,
1129      null,
1130      Stanza.node("query", Stanza.NS.roster, null, item)
1131    );
1132    this._account.sendStanza(s);
1133
1134    // If we are going to change the alias on the server, discard the cached
1135    // value that we got from our local sqlite storage at startup.
1136    delete this._serverAlias;
1137  },
1138
1139  /* Display name of the buddy */
1140  get contactDisplayName() {
1141    return this.buddy.contact.displayName || this.displayName;
1142  },
1143
1144  get tag() {
1145    return this._tag;
1146  },
1147  set tag(aNewTag) {
1148    let oldTag = this._tag;
1149    if (oldTag.name == aNewTag.name) {
1150      this.ERROR("attempting to set the tag to the same value");
1151      return;
1152    }
1153
1154    this._tag = aNewTag;
1155    Services.contacts.accountBuddyMoved(this, oldTag, aNewTag);
1156
1157    if (!this._rosterItem) {
1158      this.ERROR(
1159        "attempting to change the tag of an account buddy without roster item"
1160      );
1161      return;
1162    }
1163
1164    let item = this._rosterItem;
1165    let oldXML = item.getXML();
1166    // Remove the old tag if it was listed in the roster item.
1167    item.children = item.children.filter(
1168      c => c.qName != "group" || c.innerText != oldTag.name
1169    );
1170    // Ensure the new tag is listed.
1171    let newTagName = aNewTag.name;
1172    if (!item.getChildren("group").some(g => g.innerText == newTagName)) {
1173      item.addChild(Stanza.node("group", null, null, newTagName));
1174    }
1175    // Avoid sending anything to the server if the roster item hasn't changed.
1176    // It's possible that the roster item hasn't changed if the roster
1177    // item had several groups and the user moved locally the contact
1178    // to another group where it already was on the server.
1179    if (item.getXML() == oldXML) {
1180      return;
1181    }
1182
1183    let s = Stanza.iq(
1184      "set",
1185      null,
1186      null,
1187      Stanza.node("query", Stanza.NS.roster, null, item)
1188    );
1189    this._account.sendStanza(s);
1190  },
1191
1192  remove() {
1193    if (!this._account.connected) {
1194      return;
1195    }
1196
1197    let s = Stanza.iq(
1198      "set",
1199      null,
1200      null,
1201      Stanza.node(
1202        "query",
1203        Stanza.NS.roster,
1204        null,
1205        Stanza.node("item", null, {
1206          jid: this.normalizedName,
1207          subscription: "remove",
1208        })
1209      )
1210    );
1211    this._account.sendStanza(s);
1212  },
1213
1214  _photoHash: null,
1215  _saveIcon(aPhotoNode) {
1216    // Some servers seem to send a photo node without a type declared.
1217    let type = aPhotoNode.getElement(["TYPE"]);
1218    if (!type) {
1219      return;
1220    }
1221    type = type.innerText;
1222    const kExt = {
1223      "image/gif": "gif",
1224      "image/jpeg": "jpg",
1225      "image/png": "png",
1226    };
1227    if (!kExt.hasOwnProperty(type)) {
1228      return;
1229    }
1230
1231    let content = "",
1232      data = "";
1233    // Strip all characters not allowed in base64 before parsing.
1234    let parseBase64 = aBase => atob(aBase.replace(/[^A-Za-z0-9\+\/\=]/g, ""));
1235    for (let line of aPhotoNode.getElement(["BINVAL"]).innerText.split("\n")) {
1236      data += line;
1237      // Mozilla's atob() doesn't handle padding with "=" or "=="
1238      // unless it's at the end of the string, so we have to work around that.
1239      if (line.endsWith("=")) {
1240        content += parseBase64(data);
1241        data = "";
1242      }
1243    }
1244    content += parseBase64(data);
1245
1246    // Store a sha1 hash of the photo we have just received.
1247    let ch = Cc["@mozilla.org/security/hash;1"].createInstance(
1248      Ci.nsICryptoHash
1249    );
1250    ch.init(ch.SHA1);
1251    let dataArray = Object.keys(content).map(i => content.charCodeAt(i));
1252    ch.update(dataArray, dataArray.length);
1253    let hash = ch.finish(false);
1254    function toHexString(charCode) {
1255      return ("0" + charCode.toString(16)).slice(-2);
1256    }
1257    this._photoHash = Object.keys(hash)
1258      .map(i => toHexString(hash.charCodeAt(i)))
1259      .join("");
1260
1261    let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
1262      Ci.nsIStringInputStream
1263    );
1264    istream.setData(content, content.length);
1265
1266    let fileName = this._photoHash + "." + kExt[type];
1267    let file = FileUtils.getFile("ProfD", [
1268      "icons",
1269      this.account.protocol.normalizedName,
1270      this.account.normalizedName,
1271      fileName,
1272    ]);
1273    let ostream = FileUtils.openSafeFileOutputStream(file);
1274    let buddy = this;
1275    NetUtil.asyncCopy(istream, ostream, function(rc) {
1276      if (Components.isSuccessCode(rc)) {
1277        buddy.buddyIconFilename = Services.io.newFileURI(file).spec;
1278      }
1279    });
1280  },
1281
1282  _preferredResource: undefined,
1283  _resources: null,
1284  onAccountDisconnected() {
1285    delete this._preferredResource;
1286    delete this._resources;
1287  },
1288  // Called by the account when a presence stanza is received for this buddy.
1289  onPresenceStanza(aStanza) {
1290    let preferred = this._preferredResource;
1291
1292    // Facebook chat's XMPP server doesn't send resources, let's
1293    // replace undefined resources with empty resources.
1294    let resource =
1295      this._account._parseJID(aStanza.attributes.from).resource || "";
1296
1297    let type = aStanza.attributes.type;
1298
1299    // Reset typing status if the buddy is in a conversation and becomes unavailable.
1300    let conv = this._account._conv.get(this.normalizedName);
1301    if (type == "unavailable" && conv) {
1302      conv.updateTyping(Ci.prplIConvIM.NOT_TYPING, this.contactDisplayName);
1303    }
1304
1305    if (type == "unavailable" || type == "error") {
1306      if (!this._resources || !(resource in this._resources)) {
1307        // Ignore for already offline resources.
1308        return;
1309      }
1310      delete this._resources[resource];
1311      if (preferred == resource) {
1312        preferred = undefined;
1313      }
1314    } else {
1315      let statusInfo = parseStatus(aStanza);
1316      let priority = aStanza.getElement(["priority"]);
1317      priority = priority ? parseInt(priority.innerText, 10) : 0;
1318
1319      if (!this._resources) {
1320        this._resources = {};
1321      }
1322      this._resources[resource] = {
1323        statusType: statusInfo.statusType,
1324        statusText: statusInfo.statusText,
1325        idleSince: statusInfo.idleSince,
1326        priority,
1327        stanza: aStanza,
1328      };
1329    }
1330
1331    let photo = aStanza.getElement(["x", "photo"]);
1332    if (photo && photo.uri == Stanza.NS.vcard_update) {
1333      let hash = photo.innerText;
1334      if (hash && hash != this._photoHash) {
1335        this._account._addVCardRequest(this.normalizedName);
1336      } else if (!hash && this._photoHash) {
1337        delete this._photoHash;
1338        this.buddyIconFilename = "";
1339      }
1340    }
1341
1342    for (let r in this._resources) {
1343      if (
1344        preferred === undefined ||
1345        this._resources[r].statusType > this._resources[preferred].statusType
1346      ) {
1347        // FIXME also compare priorities...
1348        preferred = r;
1349      }
1350    }
1351    if (
1352      preferred != undefined &&
1353      preferred == this._preferredResource &&
1354      resource != preferred
1355    ) {
1356      // The presence information change is only for an unused resource,
1357      // only potential buddy tooltips need to be refreshed.
1358      this._notifyObservers("status-detail-changed");
1359      return;
1360    }
1361
1362    // Presence info has changed enough that if we are having a
1363    // conversation with one resource of this buddy, we should send
1364    // the next message to all resources.
1365    // FIXME: the test here isn't exactly right...
1366    if (
1367      this._preferredResource != preferred &&
1368      this._account._conv.has(this.normalizedName)
1369    ) {
1370      delete this._account._conv.get(this.normalizedName)._targetResource;
1371    }
1372
1373    this._preferredResource = preferred;
1374    if (preferred === undefined) {
1375      let statusType = Ci.imIStatusInfo.STATUS_UNKNOWN;
1376      if (type == "unavailable") {
1377        statusType = Ci.imIStatusInfo.STATUS_OFFLINE;
1378      }
1379      this.setStatus(statusType, "");
1380    } else {
1381      preferred = this._resources[preferred];
1382      this.setStatus(preferred.statusType, preferred.statusText);
1383    }
1384  },
1385
1386  /* Can send messages to buddies who appear offline */
1387  get canSendMessage() {
1388    return this.account.connected;
1389  },
1390
1391  /* Called when the user wants to chat with the buddy */
1392  createConversation() {
1393    return this._account.createConversation(this.normalizedName);
1394  },
1395};
1396function XMPPAccountBuddy(aAccount, aBuddy, aTag, aUserName) {
1397  this._init(aAccount, aBuddy, aTag, aUserName);
1398}
1399XMPPAccountBuddy.prototype = XMPPAccountBuddyPrototype;
1400
1401var XMPPRoomInfoPrototype = {
1402  __proto__: ClassInfo("prplIRoomInfo", "XMPP RoomInfo Object"),
1403  get topic() {
1404    return "";
1405  },
1406  get participantCount() {
1407    return Ci.prplIRoomInfo.NO_PARTICIPANT_COUNT;
1408  },
1409  get chatRoomFieldValues() {
1410    let roomJid = this._account._roomList.get(this.name);
1411    return this._account.getChatRoomDefaultFieldValues(roomJid);
1412  },
1413};
1414function XMPPRoomInfo(aName, aAccount) {
1415  this.name = aName;
1416  this._account = aAccount;
1417}
1418XMPPRoomInfo.prototype = XMPPRoomInfoPrototype;
1419
1420/* Helper class for account */
1421var XMPPAccountPrototype = {
1422  __proto__: GenericAccountPrototype,
1423
1424  _jid: null, // parsed Jabber ID: node, domain, resource
1425  _connection: null, // XMPPSession socket
1426  authMechanisms: null, // hook to let prpls tweak the list of auth mechanisms
1427
1428  // Contains the domain of MUC service which is obtained using service
1429  // discovery.
1430  _mucService: null,
1431
1432  // Maps room names to room jid.
1433  _roomList: new Map(),
1434
1435  // Callbacks used when roomInfo is available.
1436  _roomInfoCallbacks: new Set(),
1437
1438  // Determines if roomInfo that we have is expired or not.
1439  _lastListTime: 0,
1440  get isRoomInfoStale() {
1441    return Date.now() - this._lastListTime > kListRefreshInterval;
1442  },
1443
1444  // If true, we are waiting for replies.
1445  _pendingList: false,
1446
1447  // An array of jids for which we still need to request vCards.
1448  _pendingVCardRequests: [],
1449
1450  // XEP-0280: Message Carbons.
1451  // If true, message carbons are currently enabled.
1452  _isCarbonsEnabled: false,
1453
1454  /* Generate unique id for a stanza. Using id and unique sid is defined in
1455   * RFC 6120 (Section 8.2.3, 4.7.3).
1456   */
1457  generateId: () =>
1458    UuidGenerator.generateUUID()
1459      .toString()
1460      .slice(1, -1),
1461
1462  _init(aProtoInstance, aImAccount) {
1463    GenericAccountPrototype._init.call(this, aProtoInstance, aImAccount);
1464
1465    // Ongoing conversations.
1466    // The keys of this._conv are assumed to be normalized like account@domain
1467    // for normal conversations and like room@domain/nick for MUC participant
1468    // convs.
1469    this._conv = new NormalizedMap(this.normalizeFullJid.bind(this));
1470
1471    this._buddies = new NormalizedMap(this.normalize.bind(this));
1472    this._mucs = new NormalizedMap(this.normalize.bind(this));
1473  },
1474
1475  get canJoinChat() {
1476    return true;
1477  },
1478  chatRoomFields: {
1479    room: {
1480      get label() {
1481        return _("chatRoomField.room");
1482      },
1483      required: true,
1484    },
1485    server: {
1486      get label() {
1487        return _("chatRoomField.server");
1488      },
1489      required: true,
1490    },
1491    nick: {
1492      get label() {
1493        return _("chatRoomField.nick");
1494      },
1495      required: true,
1496    },
1497    password: {
1498      get label() {
1499        return _("chatRoomField.password");
1500      },
1501      isPassword: true,
1502    },
1503  },
1504  parseDefaultChatName(aDefaultChatName) {
1505    if (!aDefaultChatName) {
1506      return { nick: this._jid.node };
1507    }
1508
1509    let params = aDefaultChatName.trim().split(/\s+/);
1510    let jid = this._parseJID(params[0]);
1511
1512    // We swap node and domain as domain is required for parseJID, but node and
1513    // resource are optional. In MUC join command, Node is required as it
1514    // represents a room, but domain and resource are optional as we get muc
1515    // domain from service discovery.
1516    if (!jid.node && jid.domain) {
1517      [jid.node, jid.domain] = [jid.domain, jid.node];
1518    }
1519
1520    let chatFields = {
1521      room: jid.node,
1522      server: jid.domain || this._mucService,
1523      nick: jid.resource || this._jid.node,
1524    };
1525    if (params.length > 1) {
1526      chatFields.password = params[1];
1527    }
1528    return chatFields;
1529  },
1530  getChatRoomDefaultFieldValues(aDefaultChatName) {
1531    let rv = GenericAccountPrototype.getChatRoomDefaultFieldValues.call(
1532      this,
1533      aDefaultChatName
1534    );
1535    if (!rv.values.nick) {
1536      rv.values.nick = this._jid.node;
1537    }
1538    if (!rv.values.server && this._mucService) {
1539      rv.values.server = this._mucService;
1540    }
1541
1542    return rv;
1543  },
1544
1545  // XEP-0045: Requests joining room if it exists or
1546  // creating room if it does not exist.
1547  joinChat(aComponents) {
1548    let jid =
1549      aComponents.getValue("room") + "@" + aComponents.getValue("server");
1550    let nick = aComponents.getValue("nick");
1551
1552    let muc = this._mucs.get(jid);
1553    if (muc) {
1554      if (!muc.left) {
1555        // We are already in this conversation.
1556        return muc;
1557      } else if (!muc.chatRoomFields) {
1558        // We are rejoining a room that was parted by the user.
1559        muc._rejoined = true;
1560      }
1561    } else {
1562      muc = new this._MUCConversationConstructor(this, jid, nick);
1563      this._mucs.set(jid, muc);
1564    }
1565
1566    // Store the prplIChatRoomFieldValues to enable later reconnections.
1567    muc.chatRoomFields = aComponents;
1568    muc.joining = true;
1569    muc.removeAllParticipants();
1570
1571    let password = aComponents.getValue("password");
1572    let x = Stanza.node(
1573      "x",
1574      Stanza.NS.muc,
1575      null,
1576      password ? Stanza.node("password", null, null, password) : null
1577    );
1578    let logString;
1579    if (password) {
1580      logString =
1581        "<presence .../> (Stanza containing password to join MUC " +
1582        jid +
1583        "/" +
1584        nick +
1585        " not logged)";
1586    }
1587    this.sendStanza(
1588      Stanza.presence({ to: jid + "/" + nick }, x),
1589      undefined,
1590      undefined,
1591      logString
1592    );
1593    return muc;
1594  },
1595
1596  _idleSince: 0,
1597  observe(aSubject, aTopic, aData) {
1598    if (aTopic == "idle-time-changed") {
1599      let idleTime = parseInt(aData, 10);
1600      if (idleTime) {
1601        this._idleSince = Math.floor(Date.now() / 1000) - idleTime;
1602      } else {
1603        delete this._idleSince;
1604      }
1605      this._shouldSendPresenceForIdlenessChange = true;
1606      executeSoon(
1607        function() {
1608          if ("_shouldSendPresenceForIdlenessChange" in this) {
1609            this._sendPresence();
1610          }
1611        }.bind(this)
1612      );
1613    } else if (aTopic == "status-changed") {
1614      this._sendPresence();
1615    } else if (aTopic == "user-icon-changed") {
1616      delete this._cachedUserIcon;
1617      this._forceUserIconUpdate = true;
1618      this._sendVCard();
1619    } else if (aTopic == "user-display-name-changed") {
1620      this._forceUserDisplayNameUpdate = true;
1621    }
1622    this._sendVCard();
1623  },
1624
1625  /* GenericAccountPrototype events */
1626  /* Connect to the server */
1627  connect() {
1628    this._jid = this._parseJID(this.name);
1629
1630    // For the resource, if the user has edited the option, always use that.
1631    if (this.prefs.prefHasUserValue("resource")) {
1632      let resource = this.getString("resource");
1633
1634      // this._jid needs to be updated. This value is however never used
1635      // because while connected it's the jid of the session that's
1636      // interesting.
1637      this._jid = this._setJID(this._jid.domain, this._jid.node, resource);
1638    } else if (this._jid.resource) {
1639      // If there is a resource in the account name (inherited from libpurple),
1640      // migrate it to the pref so it appears correctly in the advanced account
1641      // options next time.
1642      this.prefs.setStringPref("resource", this._jid.resource);
1643    }
1644
1645    this._connection = new XMPPSession(
1646      this.getString("server") || this._jid.domain,
1647      this.getInt("port") || 5222,
1648      this.getString("connection_security"),
1649      this._jid,
1650      this.imAccount.password,
1651      this
1652    );
1653  },
1654
1655  remove() {
1656    this._conv.forEach(conv => conv.close());
1657    this._mucs.forEach(muc => muc.close());
1658    this._buddies.forEach((buddy, jid) => this._forgetRosterItem(jid));
1659  },
1660
1661  unInit() {
1662    if (this._connection) {
1663      this._disconnect(undefined, undefined, true);
1664    }
1665    delete this._jid;
1666    delete this._conv;
1667    delete this._buddies;
1668    delete this._mucs;
1669  },
1670
1671  /* Disconnect from the server */
1672  disconnect() {
1673    this._disconnect();
1674  },
1675
1676  addBuddy(aTag, aName) {
1677    if (!this._connection) {
1678      throw new Error("The account isn't connected");
1679    }
1680
1681    let jid = this.normalize(aName);
1682    if (!jid || !jid.includes("@")) {
1683      throw new Error("Invalid username");
1684    }
1685
1686    if (this._buddies.has(jid)) {
1687      let subscription = this._buddies.get(jid).subscription;
1688      if (subscription && (subscription == "both" || subscription == "to")) {
1689        this.DEBUG("not re-adding an existing buddy");
1690        return;
1691      }
1692    } else {
1693      let s = Stanza.iq(
1694        "set",
1695        null,
1696        null,
1697        Stanza.node(
1698          "query",
1699          Stanza.NS.roster,
1700          null,
1701          Stanza.node(
1702            "item",
1703            null,
1704            { jid },
1705            Stanza.node("group", null, null, aTag.name)
1706          )
1707        )
1708      );
1709      this.sendStanza(
1710        s,
1711        this._handleResult({
1712          default: aError => {
1713            this.WARN(
1714              "Unable to add a roster item due to " + aError + " error."
1715            );
1716          },
1717        })
1718      );
1719    }
1720    this.sendStanza(Stanza.presence({ to: jid, type: "subscribe" }));
1721  },
1722
1723  /* Loads a buddy from the local storage.
1724   * Called for each buddy locally stored before connecting
1725   * to the server. */
1726  loadBuddy(aBuddy, aTag) {
1727    let buddy = new this._accountBuddyConstructor(this, aBuddy, aTag);
1728    this._buddies.set(buddy.normalizedName, buddy);
1729    return buddy;
1730  },
1731
1732  /* Replies to a buddy request in order to accept it or deny it. */
1733  replyToBuddyRequest(aReply, aRequest) {
1734    if (!this._connection) {
1735      return;
1736    }
1737    let s = Stanza.presence({ to: aRequest.userName, type: aReply });
1738    this.sendStanza(s);
1739    this.removeBuddyRequest(aRequest);
1740  },
1741
1742  requestBuddyInfo(aJid) {
1743    if (!this.connected) {
1744      Services.obs.notifyObservers(EmptyEnumerator, "user-info-received", aJid);
1745      return;
1746    }
1747
1748    let userName;
1749    let tooltipInfo = [];
1750    let jid = this._parseJID(aJid);
1751    let muc = this._mucs.get(jid.node + "@" + jid.domain);
1752    let participant;
1753    if (muc) {
1754      participant = muc._participants.get(jid.resource);
1755      if (participant) {
1756        if (participant.accountJid) {
1757          userName = participant.accountJid;
1758        }
1759        if (!muc.left) {
1760          let statusType = participant.statusType;
1761          let statusText = participant.statusText;
1762          tooltipInfo.push(
1763            new TooltipInfo(statusType, statusText, Ci.prplITooltipInfo.status)
1764          );
1765
1766          if (participant.buddyIconFilename) {
1767            tooltipInfo.push(
1768              new TooltipInfo(
1769                null,
1770                participant.buddyIconFilename,
1771                Ci.prplITooltipInfo.icon
1772              )
1773            );
1774          }
1775        }
1776      }
1777    }
1778    Services.obs.notifyObservers(
1779      new nsSimpleEnumerator(tooltipInfo),
1780      "user-info-received",
1781      aJid
1782    );
1783
1784    let iq = Stanza.iq(
1785      "get",
1786      null,
1787      aJid,
1788      Stanza.node("vCard", Stanza.NS.vcard)
1789    );
1790    this.sendStanza(iq, aStanza => {
1791      let vCardInfo = {};
1792      let vCardNode = aStanza.getElement(["vCard"]);
1793
1794      // In the case of an error response, we just notify the observers with
1795      // what info we already have.
1796      if (aStanza.attributes.type == "result" && vCardNode) {
1797        vCardInfo = this.parseVCard(vCardNode);
1798      }
1799
1800      // The real jid of participant which is of the form local@domain/resource.
1801      // We consider the jid is provided by server is more correct than jid is
1802      // set by the user.
1803      if (userName) {
1804        vCardInfo.userName = userName;
1805      }
1806
1807      // vCard fields we want to display in the tooltip.
1808      const kTooltipFields = [
1809        "userName",
1810        "fullName",
1811        "nickname",
1812        "title",
1813        "organization",
1814        "email",
1815        "birthday",
1816        "locality",
1817        "country",
1818        "telephone",
1819      ];
1820
1821      let tooltipInfo = [];
1822      for (let field of kTooltipFields) {
1823        if (vCardInfo.hasOwnProperty(field)) {
1824          tooltipInfo.push(
1825            new TooltipInfo(_("tooltip." + field), vCardInfo[field])
1826          );
1827        }
1828      }
1829      if (vCardInfo.photo) {
1830        let dataURI = this._getPhotoURI(vCardInfo.photo);
1831
1832        // Store the photo URI for this participant.
1833        if (participant) {
1834          participant.buddyIconFilename = dataURI;
1835        }
1836
1837        tooltipInfo.push(
1838          new TooltipInfo(null, dataURI, Ci.prplITooltipInfo.icon)
1839        );
1840      }
1841      Services.obs.notifyObservers(
1842        new nsSimpleEnumerator(tooltipInfo),
1843        "user-info-received",
1844        aJid
1845      );
1846    });
1847  },
1848
1849  // Parses the photo node of a received vCard if exists and returns string of
1850  // data URI, otherwise returns null.
1851  _getPhotoURI(aPhotoNode) {
1852    if (!aPhotoNode) {
1853      return null;
1854    }
1855
1856    let type = aPhotoNode.getElement(["TYPE"]);
1857    let value = aPhotoNode.getElement(["BINVAL"]);
1858    if (!type || !value) {
1859      return null;
1860    }
1861
1862    return "data:" + type.innerText + ";base64," + value.innerText;
1863  },
1864
1865  // Parses the vCard into the properties of the returned object.
1866  parseVCard(aVCardNode) {
1867    // XEP-0054: vcard-temp.
1868    let aResult = {};
1869    for (let node of aVCardNode.children.filter(
1870      child => child.type == "node"
1871    )) {
1872      let localName = node.localName;
1873      let innerText = node.innerText;
1874      if (innerText) {
1875        if (localName == "FN") {
1876          aResult.fullName = innerText;
1877        } else if (localName == "NICKNAME") {
1878          aResult.nickname = innerText;
1879        } else if (localName == "TITLE") {
1880          aResult.title = innerText;
1881        } else if (localName == "BDAY") {
1882          aResult.birthday = innerText;
1883        } else if (localName == "JABBERID") {
1884          aResult.userName = innerText;
1885        }
1886      }
1887      if (localName == "ORG") {
1888        let organization = node.getElement(["ORGNAME"]);
1889        if (organization && organization.innerText) {
1890          aResult.organization = organization.innerText;
1891        }
1892      } else if (localName == "EMAIL") {
1893        let userID = node.getElement(["USERID"]);
1894        if (userID && userID.innerText) {
1895          aResult.email = userID.innerText;
1896        }
1897      } else if (localName == "ADR") {
1898        let locality = node.getElement(["LOCALITY"]);
1899        if (locality && locality.innerText) {
1900          aResult.locality = locality.innerText;
1901        }
1902
1903        let country = node.getElement(["CTRY"]);
1904        if (country && country.innerText) {
1905          aResult.country = country.innerText;
1906        }
1907      } else if (localName == "PHOTO") {
1908        aResult.photo = node;
1909      } else if (localName == "TEL") {
1910        let number = node.getElement(["NUMBER"]);
1911        if (number && number.innerText) {
1912          aResult.telephone = number.innerText;
1913        }
1914      }
1915      // TODO: Parse the other fields of vCard and display it in system messages
1916      // in response to /whois.
1917    }
1918    return aResult;
1919  },
1920
1921  // Returns undefined if not an error stanza, and an object
1922  // describing the error otherwise:
1923  parseError(aStanza) {
1924    if (aStanza.attributes.type != "error") {
1925      return undefined;
1926    }
1927
1928    let retval = { stanza: aStanza };
1929    let error = aStanza.getElement(["error"]);
1930
1931    // RFC 6120 Section 8.3.2: Type must be one of
1932    // auth -- retry after providing credentials
1933    // cancel -- do not retry (the error cannot be remedied)
1934    // continue -- proceed (the condition was only a warning)
1935    // modify -- retry after changing the data sent
1936    // wait -- retry after waiting (the error is temporary).
1937    retval.type = error.attributes.type;
1938
1939    // RFC 6120 Section 8.3.3.
1940    const kDefinedConditions = [
1941      "bad-request",
1942      "conflict",
1943      "feature-not-implemented",
1944      "forbidden",
1945      "gone",
1946      "internal-server-error",
1947      "item-not-found",
1948      "jid-malformed",
1949      "not-acceptable",
1950      "not-allowed",
1951      "not-authorized",
1952      "policy-violation",
1953      "recipient-unavailable",
1954      "redirect",
1955      "registration-required",
1956      "remote-server-not-found",
1957      "remote-server-timeout",
1958      "resource-constraint",
1959      "service-unavailable",
1960      "subscription-required",
1961      "undefined-condition",
1962      "unexpected-request",
1963    ];
1964    let condition = kDefinedConditions.find(c => error.getElement([c]));
1965    if (!condition) {
1966      // RFC 6120 Section 8.3.2.
1967      this.WARN(
1968        "Nonstandard or missing defined-condition element in error stanza."
1969      );
1970      condition = "undefined-condition";
1971    }
1972    retval.condition = condition;
1973
1974    let errortext = error.getElement(["text"]);
1975    if (errortext) {
1976      retval.text = errortext.innerText;
1977    }
1978
1979    return retval;
1980  },
1981
1982  // Returns an error-handling callback for use with sendStanza generated
1983  // from aHandlers, an object defining the error handlers.
1984  // If the stanza passed to the callback is an error stanza, it checks if
1985  // aHandlers contains a property with the name of the defined condition
1986  // of the error.
1987  // * If the property is a function, it is called with the parsed error
1988  //   as its argument, bound to aThis (if provided).
1989  //   It should return true if the error was handled.
1990  // * If the property is a string, it is displayed as a system message
1991  //   in the conversation given by aThis.
1992  handleErrors(aHandlers, aThis) {
1993    return aStanza => {
1994      if (!aHandlers) {
1995        return false;
1996      }
1997
1998      let error = this.parseError(aStanza);
1999      if (!error) {
2000        return false;
2001      }
2002
2003      let toCamelCase = aStr => {
2004        // Turn defined condition string into a valid camelcase
2005        // JS property name.
2006        let capitalize = s => s[0].toUpperCase() + s.slice(1);
2007        let uncapitalize = s => s[0].toLowerCase() + s.slice(1);
2008        return uncapitalize(
2009          aStr
2010            .split("-")
2011            .map(capitalize)
2012            .join("")
2013        );
2014      };
2015      let condition = toCamelCase(error.condition);
2016      // Check if we have a handler property for this kind of error or a
2017      // default handler.
2018      if (!(condition in aHandlers) && !("default" in aHandlers)) {
2019        return false;
2020      }
2021
2022      // Try to get the handler for condition, if we cannot get it, try to get
2023      // the default handler.
2024      let handler = aHandlers[condition];
2025      if (!handler) {
2026        handler = aHandlers.default;
2027      }
2028
2029      if (typeof handler == "string") {
2030        // The string is an error message to be displayed in the conversation.
2031        if (!aThis || !aThis.writeMessage) {
2032          this.ERROR(
2033            "HandleErrors was passed an error message string, but " +
2034              "no conversation to display it in:\n" +
2035              handler
2036          );
2037          return true;
2038        }
2039        aThis.writeMessage(aThis.name, handler, { system: true, error: true });
2040        return true;
2041      } else if (typeof handler == "function") {
2042        // If we're given a function, call this error handler.
2043        return handler.call(aThis, error);
2044      }
2045
2046      // If this happens, there's a bug somewhere.
2047      this.ERROR(
2048        "HandleErrors was passed a handler for '" +
2049          condition +
2050          "'' which is neither a function nor a string."
2051      );
2052      return false;
2053    };
2054  },
2055
2056  // Returns a callback suitable for use in sendStanza, to handle type==result
2057  // responses. aHandlers and aThis are passed on to handleErrors for error
2058  // handling.
2059  _handleResult(aHandlers, aThis) {
2060    return aStanza => {
2061      if (aStanza.attributes.type == "result") {
2062        return true;
2063      }
2064      return this.handleErrors(aHandlers, aThis)(aStanza);
2065    };
2066  },
2067
2068  /* XMPPSession events */
2069
2070  /* Called when the XMPP session is started */
2071  onConnection() {
2072    // Request the roster. The account will be marked as connected when this is
2073    // complete.
2074    this.reportConnecting(_("connection.downloadingRoster"));
2075    let s = Stanza.iq(
2076      "get",
2077      null,
2078      null,
2079      Stanza.node("query", Stanza.NS.roster)
2080    );
2081    this.sendStanza(s, this.onRoster, this);
2082
2083    // XEP-0030 and XEP-0045 (6): Service Discovery.
2084    // Queries Server for Associated Services.
2085    let iq = Stanza.iq(
2086      "get",
2087      null,
2088      this._jid.domain,
2089      Stanza.node("query", Stanza.NS.disco_items)
2090    );
2091    this.sendStanza(iq, this.onServiceDiscovery, this);
2092
2093    // XEP-0030: Service Discovery Information Features.
2094    iq = Stanza.iq(
2095      "get",
2096      null,
2097      this._jid.domain,
2098      Stanza.node("query", Stanza.NS.disco_info)
2099    );
2100    this.sendStanza(iq, this.onServiceDiscoveryInfo, this);
2101  },
2102
2103  /* Called whenever a stanza is received */
2104  onXmppStanza(aStanza) {},
2105
2106  /* Called when a iq stanza is received */
2107  onIQStanza(aStanza) {
2108    let type = aStanza.attributes.type;
2109    if (type == "set") {
2110      for (let query of aStanza.getChildren("query")) {
2111        if (query.uri != Stanza.NS.roster) {
2112          continue;
2113        }
2114
2115        // RFC 6121 2.1.6 (Roster push):
2116        // A receiving client MUST ignore the stanza unless it has no 'from'
2117        // attribute (i.e., implicitly from the bare JID of the user's
2118        // account) or it has a 'from' attribute whose value matches the
2119        // user's bare JID <user@domainpart>.
2120        let from = aStanza.attributes.from;
2121        if (from && from != this._jid.node + "@" + this._jid.domain) {
2122          this.WARN("Ignoring potentially spoofed roster push.");
2123          return;
2124        }
2125
2126        for (let item of query.getChildren("item")) {
2127          this._onRosterItem(item, true);
2128        }
2129        return;
2130      }
2131    } else if (type == "get") {
2132      let id = aStanza.attributes.id;
2133      let from = aStanza.attributes.from;
2134
2135      // XEP-0199: XMPP server-to-client ping (XEP-0199)
2136      let ping = aStanza.getElement(["ping"]);
2137      if (ping && ping.uri == Stanza.NS.ping) {
2138        if (from == this._jid.domain) {
2139          this.sendStanza(Stanza.iq("result", id, this._jid.domain));
2140        }
2141        return;
2142      }
2143
2144      let query = aStanza.getElement(["query"]);
2145      if (query && query.uri == Stanza.NS.version) {
2146        // XEP-0092: Software Version.
2147        let children = [];
2148        children.push(Stanza.node("name", null, null, Services.appinfo.name));
2149        children.push(
2150          Stanza.node("version", null, null, Services.appinfo.version)
2151        );
2152        let versionQuery = Stanza.node(
2153          "query",
2154          Stanza.NS.version,
2155          null,
2156          children
2157        );
2158        this.sendStanza(Stanza.iq("result", id, from, versionQuery));
2159        return;
2160      }
2161      if (query && query.uri == Stanza.NS.disco_info) {
2162        // XEP-0030: Service Discovery.
2163        let children = [];
2164        if (aStanza.attributes.node == Stanza.NS.muc_rooms) {
2165          // XEP-0045 (6.7): Room query.
2166          // TODO: Currently, we return an empty <query/> element, but we
2167          // should return non-private rooms.
2168        } else {
2169          children = SupportedFeatures.map(feature =>
2170            Stanza.node("feature", null, { var: feature })
2171          );
2172          children.unshift(
2173            Stanza.node("identity", null, {
2174              category: "client",
2175              type: "pc",
2176              name: Services.appinfo.name,
2177            })
2178          );
2179        }
2180        let discoveryQuery = Stanza.node(
2181          "query",
2182          Stanza.NS.disco_info,
2183          null,
2184          children
2185        );
2186        this.sendStanza(Stanza.iq("result", id, from, discoveryQuery));
2187        return;
2188      }
2189    }
2190    this.WARN(`Unhandled IQ ${type} stanza.`);
2191  },
2192
2193  /* Called when a presence stanza is received */
2194  onPresenceStanza(aStanza) {
2195    let from = aStanza.attributes.from;
2196    this.DEBUG("Received presence stanza for " + from);
2197
2198    let jid = this.normalize(from);
2199    let type = aStanza.attributes.type;
2200    if (type == "subscribe") {
2201      this.addBuddyRequest(
2202        jid,
2203        this.replyToBuddyRequest.bind(this, "subscribed"),
2204        this.replyToBuddyRequest.bind(this, "unsubscribed")
2205      );
2206    } else if (
2207      type == "unsubscribe" ||
2208      type == "unsubscribed" ||
2209      type == "subscribed"
2210    ) {
2211      // Nothing useful to do for these presence stanzas, as we will also
2212      // receive a roster push containing more or less the same information
2213    } else if (this._buddies.has(jid)) {
2214      this._buddies.get(jid).onPresenceStanza(aStanza);
2215    } else if (this._mucs.has(jid)) {
2216      this._mucs.get(jid).onPresenceStanza(aStanza);
2217    } else if (jid != this.normalize(this._connection._jid.jid)) {
2218      this.WARN("received presence stanza for unknown buddy " + from);
2219    } else if (
2220      jid == this._jid.node + "@" + this._jid.domain &&
2221      this._connection._resource != this._parseJID(from).resource
2222    ) {
2223      // Ignore presence stanzas for another resource.
2224    } else {
2225      this.WARN("Unhandled presence stanza.");
2226    }
2227  },
2228
2229  // XEP-0030: Discovering services and their features that are supported by
2230  // the server.
2231  onServiceDiscovery(aStanza) {
2232    let query = aStanza.getElement(["query"]);
2233    if (
2234      aStanza.attributes.type != "result" ||
2235      !query ||
2236      query.uri != Stanza.NS.disco_items
2237    ) {
2238      this.LOG("Could not get services for this server: " + this._jid.domain);
2239      return true;
2240    }
2241
2242    // Discovering the Features that are Supported by each service.
2243    query.getElements(["item"]).forEach(item => {
2244      let jid = item.attributes.jid;
2245      if (!jid) {
2246        return;
2247      }
2248      let iq = Stanza.iq(
2249        "get",
2250        null,
2251        jid,
2252        Stanza.node("query", Stanza.NS.disco_info)
2253      );
2254      this.sendStanza(iq, receivedStanza => {
2255        let query = receivedStanza.getElement(["query"]);
2256        let from = receivedStanza.attributes.from;
2257        if (
2258          aStanza.attributes.type != "result" ||
2259          !query ||
2260          query.uri != Stanza.NS.disco_info
2261        ) {
2262          this.LOG("Could not get features for this service: " + from);
2263          return true;
2264        }
2265        let features = query
2266          .getElements(["feature"])
2267          .map(elt => elt.attributes.var);
2268        let identity = query.getElement(["identity"]);
2269        if (
2270          identity &&
2271          identity.attributes.category == "conference" &&
2272          identity.attributes.type == "text" &&
2273          features.includes(Stanza.NS.muc)
2274        ) {
2275          // XEP-0045 (6.2): this feature is for a MUC Service.
2276          // XEP-0045 (15.2): Service Discovery Category/Type.
2277          this._mucService = from;
2278        }
2279        // TODO: Handle other services that are supported by XMPP through
2280        // their features.
2281
2282        return true;
2283      });
2284    });
2285    return true;
2286  },
2287
2288  // XEP-0030: Discovering Service Information and its features that are
2289  // supported by the server.
2290  onServiceDiscoveryInfo(aStanza) {
2291    let query = aStanza.getElement(["query"]);
2292    if (
2293      aStanza.attributes.type != "result" ||
2294      !query ||
2295      query.uri != Stanza.NS.disco_info
2296    ) {
2297      this.LOG("Could not get features for this server: " + this._jid.domain);
2298      return true;
2299    }
2300
2301    let features = query
2302      .getElements(["feature"])
2303      .map(elt => elt.attributes.var);
2304    if (features.includes(Stanza.NS.carbons)) {
2305      // XEP-0280: Message Carbons.
2306      // Enabling Carbons on server, as it's disabled by default on server.
2307      if (Services.prefs.getBoolPref("chat.xmpp.messageCarbons")) {
2308        let iqStanza = Stanza.iq(
2309          "set",
2310          null,
2311          null,
2312          Stanza.node("enable", Stanza.NS.carbons)
2313        );
2314        this.sendStanza(iqStanza, aStanza => {
2315          let error = this.parseError(aStanza);
2316          if (error) {
2317            this.WARN(
2318              "Unable to enable message carbons due to " +
2319                error.condition +
2320                " error."
2321            );
2322            return true;
2323          }
2324
2325          let type = aStanza.attributes.type;
2326          if (type != "result") {
2327            this.WARN(
2328              "Received unexpected stanza with " +
2329                type +
2330                " type " +
2331                "while enabling message carbons."
2332            );
2333            return true;
2334          }
2335
2336          this.LOG("Message carbons enabled.");
2337          this._isCarbonsEnabled = true;
2338          return true;
2339        });
2340      }
2341    }
2342    // TODO: Handle other features that are supported by the server.
2343    return true;
2344  },
2345
2346  requestRoomInfo(aCallback) {
2347    if (this._roomInfoCallbacks.has(aCallback)) {
2348      return;
2349    }
2350
2351    if (this.isRoomInfoStale && !this._pendingList) {
2352      this._roomList = new Map();
2353      this._lastListTime = Date.now();
2354      this._roomInfoCallback = aCallback;
2355      this._pendingList = true;
2356
2357      // XEP-0045 (6.3): Discovering Rooms.
2358      let iq = Stanza.iq(
2359        "get",
2360        null,
2361        this._mucService,
2362        Stanza.node("query", Stanza.NS.disco_items)
2363      );
2364      this.sendStanza(iq, this.onRoomDiscovery, this);
2365    } else {
2366      let rooms = [...this._roomList.keys()];
2367      aCallback.onRoomInfoAvailable(rooms, !this._pendingList);
2368    }
2369
2370    if (this._pendingList) {
2371      this._roomInfoCallbacks.add(aCallback);
2372    }
2373  },
2374
2375  onRoomDiscovery(aStanza) {
2376    let query = aStanza.getElement(["query"]);
2377    if (!query || query.uri != Stanza.NS.disco_items) {
2378      this.LOG("Could not get rooms for this server: " + this._jid.domain);
2379      return;
2380    }
2381
2382    // XEP-0059: Result Set Management.
2383    let set = query.getElement(["set"]);
2384    let last = set ? set.getElement(["last"]) : null;
2385    if (last) {
2386      let iq = Stanza.iq(
2387        "get",
2388        null,
2389        this._mucService,
2390        Stanza.node("query", Stanza.NS.disco_items)
2391      );
2392      this.sendStanza(iq, this.onRoomDiscovery, this);
2393    } else {
2394      this._pendingList = false;
2395    }
2396
2397    let rooms = [];
2398    query.getElements(["item"]).forEach(item => {
2399      let jid = this._parseJID(item.attributes.jid);
2400      if (!jid) {
2401        return;
2402      }
2403
2404      let name = item.attributes.name;
2405      if (!name) {
2406        name = jid.node ? jid.node : jid.jid;
2407      }
2408
2409      this._roomList.set(name, jid.jid);
2410      rooms.push(name);
2411    });
2412
2413    this._roomInfoCallback.onRoomInfoAvailable(rooms, !this._pendingList);
2414  },
2415
2416  getRoomInfo(aName) {
2417    return new XMPPRoomInfo(aName, this);
2418  },
2419
2420  // Returns null if not an invitation stanza, and an object
2421  // describing the invitation otherwise.
2422  parseInvitation(aStanza) {
2423    let x = aStanza.getElement(["x"]);
2424    if (!x) {
2425      return null;
2426    }
2427    let retVal = {};
2428
2429    // XEP-0045. Direct Invitation (7.8.1)
2430    // Described in XEP-0249.
2431    // jid (chatroom) is required.
2432    // Password, reason, continue and thread are optional.
2433    if (x.uri == Stanza.NS.conference) {
2434      if (!x.attributes.jid) {
2435        this.WARN("Received an invitation with missing MUC jid.");
2436        return null;
2437      }
2438      retVal.mucJid = this.normalize(x.attributes.jid);
2439      retVal.from = this.normalize(aStanza.attributes.from);
2440      retVal.password = x.attributes.password;
2441      retVal.reason = x.attributes.reason;
2442      retVal.continue = x.attributes.continue;
2443      retVal.thread = x.attributes.thread;
2444      return retVal;
2445    }
2446
2447    // XEP-0045. Mediated Invitation (7.8.2)
2448    // Sent by the chatroom on behalf of someone in the chatroom.
2449    // jid (chatroom) and from (inviter) are required.
2450    // password and reason are optional.
2451    if (x.uri == Stanza.NS.muc_user) {
2452      let invite = x.getElement(["invite"]);
2453      if (!invite || !invite.attributes.from) {
2454        this.WARN("Received an invitation with missing MUC invite or from.");
2455        return null;
2456      }
2457      retVal.mucJid = this.normalize(aStanza.attributes.from);
2458      retVal.from = this.normalize(invite.attributes.from);
2459      let continueElement = invite.getElement(["continue"]);
2460      retVal.continue = !!continueElement;
2461      if (continueElement) {
2462        retVal.thread = continueElement.attributes.thread;
2463      }
2464      if (x.getElement(["password"])) {
2465        retVal.password = x.getElement(["password"]).innerText;
2466      }
2467      if (invite.getElement(["reason"])) {
2468        retVal.reason = invite.getElement(["reason"]).innerText;
2469      }
2470      return retVal;
2471    }
2472
2473    return null;
2474  },
2475
2476  /* Called when a message stanza is received */
2477  onMessageStanza(aStanza) {
2478    // XEP-0280: Message Carbons.
2479    // Sending and Receiving Messages.
2480    // Indicates that the forwarded message was sent or received.
2481    let isSent = false;
2482    let carbonStanza =
2483      aStanza.getElement(["sent"]) || aStanza.getElement(["received"]);
2484    if (carbonStanza) {
2485      if (carbonStanza.uri != Stanza.NS.carbons) {
2486        this.WARN(
2487          "Received a forwarded message which does not '" +
2488            Stanza.NS.carbons +
2489            "' namespace."
2490        );
2491        return;
2492      }
2493
2494      isSent = carbonStanza.localName == "sent";
2495      carbonStanza = carbonStanza.getElement(["forwarded", "message"]);
2496      if (this._isCarbonsEnabled) {
2497        aStanza = carbonStanza;
2498      } else {
2499        this.WARN(
2500          "Received an unexpected forwarded message while message " +
2501            "carbons are not enabled."
2502        );
2503        return;
2504      }
2505    }
2506
2507    // For forwarded sent messages, we need to use "to" attribute to
2508    // get the right conversation as from in this case is this account.
2509    let convJid = isSent ? aStanza.attributes.to : aStanza.attributes.from;
2510
2511    let normConvJid = this.normalize(convJid);
2512    let isMuc = this._mucs.has(normConvJid);
2513
2514    let type = aStanza.attributes.type;
2515    let x = aStanza.getElement(["x"]);
2516    let body;
2517    let b = aStanza.getElement(["body"]);
2518    if (b) {
2519      // If there's a <body> child we have more than just typing notifications.
2520      // Prefer HTML (in <html><body>) and use plain text (<body>) as fallback.
2521      let htmlBody = aStanza.getElement(["html", "body"]);
2522      if (htmlBody) {
2523        body = htmlBody.innerXML;
2524      } else {
2525        // Even if the message is in plain text, the prplIMessage
2526        // should contain a string that's correctly escaped for
2527        // insertion in an HTML document.
2528        body = TXTToHTML(b.innerText);
2529      }
2530    }
2531
2532    let subject = aStanza.getElement(["subject"]);
2533    // Ignore subject when !isMuc. We're being permissive about subject changes
2534    // in the comment below, so we need to be careful about where that makes
2535    // sense. Psi+'s OTR plugin includes a subject and body in its message
2536    // stanzas.
2537    if (subject && isMuc) {
2538      // XEP-0045 (7.2.16): Check for a subject element in the stanza and update
2539      // the topic if it exists.
2540      // We are breaking the spec because only a message that contains a
2541      // <subject/> but no <body/> element shall be considered a subject change
2542      // for MUC, but we ignore that to be compatible with ejabberd versions
2543      // before 15.06.
2544      let muc = this._mucs.get(normConvJid);
2545      let nick = this._parseJID(convJid).resource;
2546      // TODO There can be multiple subject elements with different xml:lang
2547      // attributes.
2548      muc.setTopic(subject.innerText, nick);
2549      return;
2550    }
2551
2552    let invitation = this.parseInvitation(aStanza);
2553    if (invitation) {
2554      let messageID;
2555      if (invitation.reason) {
2556        messageID = "conversation.muc.invitationWithReason2";
2557      } else {
2558        messageID = "conversation.muc.invitationWithoutReason";
2559      }
2560      if (invitation.password) {
2561        messageID += ".password";
2562      }
2563      let params = [
2564        invitation.from,
2565        invitation.mucJid,
2566        invitation.password,
2567        invitation.reason,
2568      ].filter(s => s);
2569      let message = _(messageID, ...params);
2570
2571      if (
2572        Services.prefs.getIntPref(
2573          "messenger.conversations.autoAcceptChatInvitations"
2574        ) == 1
2575      ) {
2576        // Auto-accept the invitation.
2577        let chatRoomFields = this.getChatRoomDefaultFieldValues(
2578          invitation.mucJid
2579        );
2580        if (invitation.password) {
2581          chatRoomFields.setValue("password", invitation.password);
2582        }
2583        let muc = this.joinChat(chatRoomFields);
2584        muc.writeMessage(muc.name, message, { system: true });
2585      } else {
2586        // Otherwise, just notify the user.
2587        let conv = this.createConversation(invitation.from);
2588        if (conv) {
2589          conv.writeMessage(invitation.from, message, { system: true });
2590        }
2591      }
2592    }
2593
2594    if (body) {
2595      let date = _getDelay(aStanza);
2596      if (
2597        type == "groupchat" ||
2598        (type == "error" && isMuc && !this._conv.has(convJid))
2599      ) {
2600        if (!isMuc) {
2601          this.WARN(
2602            "Received a groupchat message for unknown MUC " + normConvJid
2603          );
2604          return;
2605        }
2606        let muc = this._mucs.get(normConvJid);
2607        muc.incomingMessage(body, aStanza, date);
2608        return;
2609      }
2610
2611      let conv = this.createConversation(convJid);
2612      if (!conv) {
2613        return;
2614      }
2615
2616      if (isSent) {
2617        _displaySentMsg(conv, body, date);
2618        return;
2619      }
2620      conv.incomingMessage(body, aStanza, date);
2621    } else if (type == "error") {
2622      let conv = this.createConversation(convJid);
2623      if (conv) {
2624        conv.incomingMessage(null, aStanza);
2625      }
2626    } else if (x && x.uri == Stanza.NS.muc_user) {
2627      let muc = this._mucs.get(normConvJid);
2628      if (!muc) {
2629        this.WARN(
2630          "Received a groupchat message for unknown MUC " + normConvJid
2631        );
2632        return;
2633      }
2634      muc.onMessageStanza(aStanza);
2635      return;
2636    }
2637
2638    // If this is a sent message carbon, the user is typing on another client.
2639    if (isSent) {
2640      return;
2641    }
2642
2643    // Don't create a conversation to only display the typing notifications.
2644    if (!this._conv.has(normConvJid) && !this._conv.has(convJid)) {
2645      return;
2646    }
2647
2648    // Ignore errors while delivering typing notifications.
2649    if (type == "error") {
2650      return;
2651    }
2652
2653    let typingState = Ci.prplIConvIM.NOT_TYPING;
2654    let state;
2655    let s = aStanza.getChildrenByNS(Stanza.NS.chatstates);
2656    if (s.length > 0) {
2657      state = s[0].localName;
2658    }
2659    if (state) {
2660      this.DEBUG(state);
2661      if (state == "composing") {
2662        typingState = Ci.prplIConvIM.TYPING;
2663      } else if (state == "paused") {
2664        typingState = Ci.prplIConvIM.TYPED;
2665      }
2666    }
2667    let convName = normConvJid;
2668
2669    // If the bare JID is a MUC that we have joined, use the full JID as this
2670    // is a private message to a MUC participant.
2671    if (isMuc) {
2672      convName = convJid;
2673    }
2674
2675    let conv = this._conv.get(convName);
2676    if (!conv) {
2677      return;
2678    }
2679    conv.updateTyping(typingState, conv.shortName);
2680    conv.supportChatStateNotifications = !!state;
2681  },
2682
2683  /** Called when there is an error in the XMPP session */
2684  onError(aError, aException) {
2685    if (aError === null || aError === undefined) {
2686      aError = Ci.prplIAccount.ERROR_OTHER_ERROR;
2687    }
2688    this._disconnect(aError, aException.toString());
2689  },
2690
2691  onVCard(aStanza) {
2692    let jid = this._pendingVCardRequests.shift();
2693    this._requestNextVCard();
2694    if (!this._buddies.has(jid)) {
2695      this.WARN("Received a vCard for unknown buddy " + jid);
2696      return;
2697    }
2698
2699    let vCard = aStanza.getElement(["vCard"]);
2700    let error = this.parseError(aStanza);
2701    if (
2702      (error &&
2703        (error.condition == "item-not-found" ||
2704          error.condition == "service-unavailable")) ||
2705      !vCard ||
2706      !vCard.children.length
2707    ) {
2708      this.LOG("No vCard exists (or the user does not exist) for " + jid);
2709      return;
2710    } else if (error) {
2711      this.WARN("Received unexpected vCard error " + error.condition);
2712      return;
2713    }
2714
2715    let buddy = this._buddies.get(jid);
2716    let stanzaJid = this.normalize(aStanza.attributes.from);
2717    if (jid && jid != stanzaJid) {
2718      this.ERROR(
2719        "Received vCard for a different jid (" +
2720          stanzaJid +
2721          ") " +
2722          "than the requested " +
2723          jid
2724      );
2725    }
2726
2727    let foundFormattedName = false;
2728    let vCardInfo = this.parseVCard(vCard);
2729    if (vCardInfo.fullName) {
2730      buddy.vCardFormattedName = vCardInfo.fullName;
2731      foundFormattedName = true;
2732    }
2733    if (vCardInfo.photo) {
2734      buddy._saveIcon(vCardInfo.photo);
2735    }
2736    if (!foundFormattedName && buddy._vCardFormattedName) {
2737      buddy.vCardFormattedName = "";
2738    }
2739    buddy._vCardReceived = true;
2740  },
2741
2742  _requestNextVCard() {
2743    if (!this._pendingVCardRequests.length) {
2744      return;
2745    }
2746    let s = Stanza.iq(
2747      "get",
2748      null,
2749      this._pendingVCardRequests[0],
2750      Stanza.node("vCard", Stanza.NS.vcard)
2751    );
2752    this.sendStanza(s, this.onVCard, this);
2753  },
2754
2755  _addVCardRequest(aJID) {
2756    let requestPending = !!this._pendingVCardRequests.length;
2757    this._pendingVCardRequests.push(aJID);
2758    if (!requestPending) {
2759      this._requestNextVCard();
2760    }
2761  },
2762
2763  // XEP-0029 (Section 2) and RFC 6122 (Section 2): The node and domain are
2764  // lowercase, while resources are case sensitive and can contain spaces.
2765  normalizeFullJid(aJID) {
2766    return this._parseJID(aJID.trim()).jid;
2767  },
2768
2769  // Standard normalization for XMPP removes the resource part of jids.
2770  normalize(aJID) {
2771    return aJID
2772      .trim()
2773      .split("/", 1)[0] // up to first slash
2774      .toLowerCase();
2775  },
2776
2777  // RFC 6122 (Section 2): [ localpart "@" ] domainpart [ "/" resourcepart ] is
2778  // the form of jid.
2779  // Localpart is parsed as node and optional.
2780  // Domainpart is parsed as domain and required.
2781  // resourcepart is parsed as resource and optional.
2782  _parseJID(aJid) {
2783    let match = /^(?:([^"&'/:<>@]+)@)?([^@/<>'\"]+)(?:\/(.*))?$/.exec(
2784      aJid.trim()
2785    );
2786    if (!match) {
2787      return null;
2788    }
2789
2790    let result = {
2791      node: match[1],
2792      domain: match[2].toLowerCase(),
2793      resource: match[3],
2794    };
2795    return this._setJID(result.domain, result.node, result.resource);
2796  },
2797
2798  // Constructs jid as an object from domain, node and resource parts.
2799  // The object has properties (node, domain, resource and jid).
2800  // aDomain is required, but aNode and aResource are optional.
2801  _setJID(aDomain, aNode = null, aResource = null) {
2802    if (!aDomain) {
2803      throw new Error("aDomain must have a value");
2804    }
2805
2806    let result = {
2807      node: aNode,
2808      domain: aDomain.toLowerCase(),
2809      resource: aResource,
2810    };
2811    let jid = result.domain;
2812    if (result.node) {
2813      result.node = result.node.toLowerCase();
2814      jid = result.node + "@" + jid;
2815    }
2816    if (result.resource) {
2817      jid += "/" + result.resource;
2818    }
2819    result.jid = jid;
2820    return result;
2821  },
2822
2823  _onRosterItem(aItem, aNotifyOfUpdates) {
2824    let jid = aItem.attributes.jid;
2825    if (!jid) {
2826      this.WARN("Received a roster item without jid: " + aItem.getXML());
2827      return "";
2828    }
2829    jid = this.normalize(jid);
2830
2831    let subscription = "";
2832    if ("subscription" in aItem.attributes) {
2833      subscription = aItem.attributes.subscription;
2834    }
2835    if (subscription == "remove") {
2836      this._forgetRosterItem(jid);
2837      return "";
2838    }
2839
2840    let buddy;
2841    if (this._buddies.has(jid)) {
2842      buddy = this._buddies.get(jid);
2843      let groups = aItem.getChildren("group");
2844      if (groups.length) {
2845        // If the server specified at least one group, ensure the group we use
2846        // as the account buddy's tag is still a group on the server...
2847        let tagName = buddy.tag.name;
2848        if (!groups.some(g => g.innerText == tagName)) {
2849          // ... otherwise we need to move our account buddy to a new group.
2850          tagName = groups[0].innerText;
2851          if (tagName) {
2852            // Should always be true, but check just in case...
2853            let oldTag = buddy.tag;
2854            buddy._tag = Services.tags.createTag(tagName);
2855            Services.contacts.accountBuddyMoved(buddy, oldTag, buddy._tag);
2856          }
2857        }
2858      }
2859    } else {
2860      let tag;
2861      for (let group of aItem.getChildren("group")) {
2862        let name = group.innerText;
2863        if (name) {
2864          tag = Services.tags.createTag(name);
2865          break; // TODO we should create an accountBuddy per group,
2866          // but this._buddies would probably not like that...
2867        }
2868      }
2869      buddy = new this._accountBuddyConstructor(
2870        this,
2871        null,
2872        tag || Services.tags.defaultTag,
2873        jid
2874      );
2875    }
2876
2877    // We request the vCard only if we haven't received it yet and are
2878    // subscribed to presence for that contact.
2879    if (
2880      (subscription == "both" || subscription == "to") &&
2881      !buddy._vCardReceived
2882    ) {
2883      this._addVCardRequest(jid);
2884    }
2885
2886    let alias = "name" in aItem.attributes ? aItem.attributes.name : "";
2887    if (alias) {
2888      if (aNotifyOfUpdates && this._buddies.has(jid)) {
2889        buddy.rosterAlias = alias;
2890      } else {
2891        buddy._rosterAlias = alias;
2892      }
2893    } else if (buddy._rosterAlias) {
2894      buddy.rosterAlias = "";
2895    }
2896
2897    if (subscription) {
2898      buddy.subscription = subscription;
2899    }
2900    if (!this._buddies.has(jid)) {
2901      this._buddies.set(jid, buddy);
2902      Services.contacts.accountBuddyAdded(buddy);
2903    } else if (aNotifyOfUpdates) {
2904      buddy._notifyObservers("status-detail-changed");
2905    }
2906
2907    // Keep the xml nodes of the item so that we don't have to
2908    // recreate them when changing something (eg. the alias) in it.
2909    buddy._rosterItem = aItem;
2910
2911    return jid;
2912  },
2913  _forgetRosterItem(aJID) {
2914    Services.contacts.accountBuddyRemoved(this._buddies.get(aJID));
2915    this._buddies.delete(aJID);
2916  },
2917
2918  /* When the roster is received */
2919  onRoster(aStanza) {
2920    // For the first element that is a roster stanza.
2921    for (let qe of aStanza.getChildren("query")) {
2922      if (qe.uri != Stanza.NS.roster) {
2923        continue;
2924      }
2925
2926      // Find all the roster items in the new message.
2927      let newRoster = new Set();
2928      for (let item of qe.getChildren("item")) {
2929        let jid = this._onRosterItem(item);
2930        if (jid) {
2931          newRoster.add(jid);
2932        }
2933      }
2934      // If an item was in the old roster, but not in the new, forget it.
2935      for (let jid of this._buddies.keys()) {
2936        if (!newRoster.has(jid)) {
2937          this._forgetRosterItem(jid);
2938        }
2939      }
2940      break;
2941    }
2942
2943    this._sendPresence();
2944    this._buddies.forEach(b => {
2945      if (b.subscription == "both" || b.subscription == "to") {
2946        b.setStatus(Ci.imIStatusInfo.STATUS_OFFLINE, "");
2947      }
2948    });
2949    this.reportConnected();
2950    this._sendVCard();
2951  },
2952
2953  /* Public methods */
2954
2955  sendStanza(aStanza, aCallback, aThis, aLogString) {
2956    return this._connection.sendStanza(aStanza, aCallback, aThis, aLogString);
2957  },
2958
2959  // Variations of the XMPP protocol can change these default constructors:
2960  _conversationConstructor: XMPPConversation,
2961  _MUCConversationConstructor: XMPPMUCConversation,
2962  _accountBuddyConstructor: XMPPAccountBuddy,
2963
2964  /* Create a new conversation */
2965  createConversation(aName) {
2966    let convName = this.normalize(aName);
2967
2968    // Checks if conversation is with a participant of a MUC we are in. We do
2969    // not want to strip the resource as it is of the form room@domain/nick.
2970    let isMucParticipant = this._mucs.has(convName);
2971    if (isMucParticipant) {
2972      convName = this.normalizeFullJid(aName);
2973    }
2974
2975    // Checking that the aName can be parsed and is not broken.
2976    let jid = this._parseJID(convName);
2977    if (
2978      !jid ||
2979      !jid.domain ||
2980      (isMucParticipant && (!jid.node || !jid.resource))
2981    ) {
2982      this.ERROR("Could not create conversation as jid is broken: " + convName);
2983      throw new Error("Invalid JID");
2984    }
2985
2986    if (!this._conv.has(convName)) {
2987      this._conv.set(
2988        convName,
2989        new this._conversationConstructor(this, convName, isMucParticipant)
2990      );
2991    }
2992
2993    return this._conv.get(convName);
2994  },
2995
2996  /* Remove an existing conversation */
2997  removeConversation(aNormalizedName) {
2998    if (this._conv.has(aNormalizedName)) {
2999      this._conv.delete(aNormalizedName);
3000    } else if (this._mucs.has(aNormalizedName)) {
3001      this._mucs.delete(aNormalizedName);
3002    }
3003  },
3004
3005  /* Private methods */
3006
3007  /* Disconnect from the server */
3008  /* The aError and aErrorMessage parameters are passed to reportDisconnecting
3009   * and used by the account manager.
3010   * The aQuiet parameter is to avoid sending status change notifications
3011   * during the uninitialization of the account. */
3012  _disconnect(
3013    aError = Ci.prplIAccount.NO_ERROR,
3014    aErrorMessage = "",
3015    aQuiet = false
3016  ) {
3017    if (!this._connection) {
3018      return;
3019    }
3020
3021    this.reportDisconnecting(aError, aErrorMessage);
3022
3023    this._buddies.forEach(b => {
3024      if (!aQuiet) {
3025        b.setStatus(Ci.imIStatusInfo.STATUS_UNKNOWN, "");
3026      }
3027      b.onAccountDisconnected();
3028    });
3029
3030    this._mucs.forEach(muc => {
3031      muc.joining = false; // In case we never finished joining.
3032      muc.left = true;
3033    });
3034
3035    this._connection.disconnect();
3036    delete this._connection;
3037
3038    // We won't receive "user-icon-changed" notifications while the
3039    // account isn't connected, so clear the cache to avoid keeping an
3040    // obsolete icon.
3041    delete this._cachedUserIcon;
3042    // Also clear the cached user vCard, as we will want to redownload it
3043    // after reconnecting.
3044    delete this._userVCard;
3045
3046    // Clear vCard requests.
3047    this._pendingVCardRequests = [];
3048
3049    this.reportDisconnected();
3050  },
3051
3052  /* Set the user status on the server */
3053  _sendPresence() {
3054    delete this._shouldSendPresenceForIdlenessChange;
3055
3056    if (!this._connection) {
3057      return;
3058    }
3059
3060    let si = this.imAccount.statusInfo;
3061    let statusType = si.statusType;
3062    let show = "";
3063    if (statusType == Ci.imIStatusInfo.STATUS_UNAVAILABLE) {
3064      show = "dnd";
3065    } else if (
3066      statusType == Ci.imIStatusInfo.STATUS_AWAY ||
3067      statusType == Ci.imIStatusInfo.STATUS_IDLE
3068    ) {
3069      show = "away";
3070    }
3071    let children = [];
3072    if (show) {
3073      children.push(Stanza.node("show", null, null, show));
3074    }
3075    let statusText = si.statusText;
3076    if (statusText) {
3077      children.push(Stanza.node("status", null, null, statusText));
3078    }
3079    if (this._idleSince) {
3080      let time = Math.floor(Date.now() / 1000) - this._idleSince;
3081      children.push(Stanza.node("query", Stanza.NS.last, { seconds: time }));
3082    }
3083    if (this.prefs.prefHasUserValue("priority")) {
3084      let priority = Math.max(-128, Math.min(127, this.getInt("priority")));
3085      if (priority) {
3086        children.push(Stanza.node("priority", null, null, priority.toString()));
3087      }
3088    }
3089    this.sendStanza(
3090      Stanza.presence({ "xml:lang": "en" }, children),
3091      aStanza => {
3092        // As we are implicitly subscribed to our own presence (rfc6121#4), we
3093        // will receive the presence stanza mirrored back to us. We don't need
3094        // to do anything with this response.
3095        return true;
3096      }
3097    );
3098  },
3099
3100  _downloadingUserVCard: false,
3101  _downloadUserVCard() {
3102    // If a download is already in progress, don't start another one.
3103    if (this._downloadingUserVCard) {
3104      return;
3105    }
3106    this._downloadingUserVCard = true;
3107    let s = Stanza.iq("get", null, null, Stanza.node("vCard", Stanza.NS.vcard));
3108    this.sendStanza(s, this.onUserVCard, this);
3109  },
3110
3111  onUserVCard(aStanza) {
3112    delete this._downloadingUserVCard;
3113    let userVCard = aStanza.getElement(["vCard"]) || null;
3114    if (userVCard) {
3115      // Strip any server-specific namespace off the incoming vcard
3116      // before storing it.
3117      this._userVCard = Stanza.node(
3118        "vCard",
3119        Stanza.NS.vcard,
3120        null,
3121        userVCard.children
3122      );
3123    }
3124
3125    // If a user icon exists in the vCard we received from the server,
3126    // we need to ensure the line breaks in its binval are exactly the
3127    // same as those we would include if we sent the icon, and that
3128    // there isn't any other whitespace.
3129    if (this._userVCard) {
3130      let binval = this._userVCard.getElement(["PHOTO", "BINVAL"]);
3131      if (binval && binval.children.length) {
3132        binval = binval.children[0];
3133        binval.text = binval.text
3134          .replace(/[^A-Za-z0-9\+\/\=]/g, "")
3135          .replace(/.{74}/g, "$&\n");
3136      }
3137    } else {
3138      // Downloading the vCard failed.
3139      if (
3140        this.handleErrors({
3141          itemNotFound: () => false, // OK, no vCard exists yet.
3142          default: () => true,
3143        })(aStanza)
3144      ) {
3145        this.WARN(
3146          "Unexpected error retrieving the user's vcard, " +
3147            "so we won't attempt to set it either."
3148        );
3149        return;
3150      }
3151      // Set this so that we don't get into an infinite loop trying to download
3152      // the vcard again. The check in sendVCard is for hasOwnProperty.
3153      this._userVCard = null;
3154    }
3155    this._sendVCard();
3156  },
3157
3158  _cachingUserIcon: false,
3159  _cacheUserIcon() {
3160    if (this._cachingUserIcon) {
3161      return;
3162    }
3163
3164    let userIcon = this.imAccount.statusInfo.getUserIcon();
3165    if (!userIcon) {
3166      this._cachedUserIcon = null;
3167      this._sendVCard();
3168      return;
3169    }
3170
3171    this._cachingUserIcon = true;
3172    let channel = NetUtil.newChannel({
3173      uri: userIcon,
3174      loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
3175      securityFlags:
3176        Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
3177      contentPolicyType: Ci.nsIContentPolicy.TYPE_IMAGE,
3178    });
3179    NetUtil.asyncFetch(channel, (inputStream, resultCode) => {
3180      if (!Components.isSuccessCode(resultCode)) {
3181        return;
3182      }
3183      try {
3184        let type = channel.contentType;
3185        let buffer = NetUtil.readInputStreamToString(
3186          inputStream,
3187          inputStream.available()
3188        );
3189        let readImage = imgTools.decodeImageFromBuffer(
3190          buffer,
3191          buffer.length,
3192          type
3193        );
3194        let scaledImage;
3195        if (readImage.width <= 96 && readImage.height <= 96) {
3196          scaledImage = imgTools.encodeImage(readImage, type);
3197        } else {
3198          if (type != "image/jpeg") {
3199            type = "image/png";
3200          }
3201          scaledImage = imgTools.encodeScaledImage(readImage, type, 64, 64);
3202        }
3203
3204        let bstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
3205          Ci.nsIBinaryInputStream
3206        );
3207        bstream.setInputStream(scaledImage);
3208
3209        let data = bstream.readBytes(bstream.available());
3210        this._cachedUserIcon = {
3211          type,
3212          binval: btoa(data).replace(/.{74}/g, "$&\n"),
3213        };
3214      } catch (e) {
3215        Cu.reportError(e);
3216        this._cachedUserIcon = null;
3217      }
3218      delete this._cachingUserIcon;
3219      this._sendVCard();
3220    });
3221  },
3222  _sendVCard() {
3223    if (!this._connection) {
3224      return;
3225    }
3226
3227    // We have to download the user's existing vCard before updating it.
3228    // This lets us preserve the fields that we don't change or don't know.
3229    // Some servers may reject a new vCard if we don't do this first.
3230    if (!this.hasOwnProperty("_userVCard")) {
3231      // The download of the vCard is asynchronous and will call _sendVCard back
3232      // when the user's vCard has been received.
3233      this._downloadUserVCard();
3234      return;
3235    }
3236
3237    // Read the local user icon asynchronously from the disk.
3238    // _cacheUserIcon will call _sendVCard back once the icon is ready.
3239    if (!this.hasOwnProperty("_cachedUserIcon")) {
3240      this._cacheUserIcon();
3241      return;
3242    }
3243
3244    // If the user currently doesn't have any vCard on the server or
3245    // the download failed, an empty new one.
3246    if (!this._userVCard) {
3247      this._userVCard = Stanza.node("vCard", Stanza.NS.vcard);
3248    }
3249
3250    // Keep a serialized copy of the existing user vCard so that we
3251    // can avoid resending identical data to the server.
3252    let existingVCard = this._userVCard.getXML();
3253
3254    let fn = this._userVCard.getElement(["FN"]);
3255    let displayName = this.imAccount.statusInfo.displayName;
3256    if (displayName) {
3257      // If a display name is set locally, update or add an FN field to the vCard.
3258      if (!fn) {
3259        this._userVCard.addChild(
3260          Stanza.node("FN", Stanza.NS.vcard, null, displayName)
3261        );
3262      } else if (fn.children.length) {
3263        fn.children[0].text = displayName;
3264      } else {
3265        fn.addText(displayName);
3266      }
3267    } else if ("_forceUserDisplayNameUpdate" in this) {
3268      // We remove a display name stored on the server without replacing
3269      // it with a new value only if this _sendVCard call is the result of
3270      // a user action. This is to avoid removing data from the server each
3271      // time the user connects from a new profile.
3272      this._userVCard.children = this._userVCard.children.filter(
3273        n => n.qName != "FN"
3274      );
3275    }
3276    delete this._forceUserDisplayNameUpdate;
3277
3278    if (this._cachedUserIcon) {
3279      // If we have a local user icon, update or add it in the PHOTO field.
3280      let photoChildren = [
3281        Stanza.node("TYPE", Stanza.NS.vcard, null, this._cachedUserIcon.type),
3282        Stanza.node(
3283          "BINVAL",
3284          Stanza.NS.vcard,
3285          null,
3286          this._cachedUserIcon.binval
3287        ),
3288      ];
3289      let photo = this._userVCard.getElement(["PHOTO"]);
3290      if (photo) {
3291        photo.children = photoChildren;
3292      } else {
3293        this._userVCard.addChild(
3294          Stanza.node("PHOTO", Stanza.NS.vcard, null, photoChildren)
3295        );
3296      }
3297    } else if ("_forceUserIconUpdate" in this) {
3298      // Like for the display name, we remove a photo without
3299      // replacing it only if the call is caused by a user action.
3300      this._userVCard.children = this._userVCard.children.filter(
3301        n => n.qName != "PHOTO"
3302      );
3303    }
3304    delete this._forceUserIconUpdate;
3305
3306    // Send the vCard only if it has really changed.
3307    // We handle the result response from the server (it does not require
3308    // any further action).
3309    if (this._userVCard.getXML() != existingVCard) {
3310      this.sendStanza(
3311        Stanza.iq("set", null, null, this._userVCard),
3312        this._handleResult()
3313      );
3314    } else {
3315      this.LOG(
3316        "Not sending the vCard because the server stored vCard is identical."
3317      );
3318    }
3319  },
3320};
3321function XMPPAccount(aProtocol, aImAccount) {
3322  this._pendingVCardRequests = [];
3323  this._init(aProtocol, aImAccount);
3324}
3325XMPPAccount.prototype = XMPPAccountPrototype;
3326