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