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 = ["AddrBookDirectory"]; 6 7const { XPCOMUtils } = ChromeUtils.import( 8 "resource://gre/modules/XPCOMUtils.jsm" 9); 10 11XPCOMUtils.defineLazyModuleGetters(this, { 12 AddrBookCard: "resource:///modules/AddrBookCard.jsm", 13 AddrBookMailingList: "resource:///modules/AddrBookMailingList.jsm", 14 compareAddressBooks: "resource:///modules/AddrBookUtils.jsm", 15 newUID: "resource:///modules/AddrBookUtils.jsm", 16 Services: "resource://gre/modules/Services.jsm", 17}); 18 19/** 20 * Abstract base class implementing nsIAbDirectory. 21 * 22 * @abstract 23 * @implements {nsIAbDirectory} 24 */ 25class AddrBookDirectory { 26 QueryInterface = ChromeUtils.generateQI(["nsIAbDirectory"]); 27 28 constructor() { 29 this._uid = null; 30 this._dirName = null; 31 } 32 33 _initialized = false; 34 init(uri) { 35 if (this._initialized) { 36 throw new Components.Exception( 37 `Directory already initialized: ${uri}`, 38 Cr.NS_ERROR_ALREADY_INITIALIZED 39 ); 40 } 41 42 // If this._readOnly is true, the user is prevented from making changes to 43 // the contacts. Subclasses may override this (for example to sync with a 44 // server) by setting this._overrideReadOnly to true, but must clear it 45 // before yielding to another thread (e.g. awaiting a Promise). 46 47 if (this._dirPrefId) { 48 XPCOMUtils.defineLazyPreferenceGetter( 49 this, 50 "_readOnly", 51 `${this.dirPrefId}.readOnly`, 52 false 53 ); 54 } 55 56 this._initialized = true; 57 } 58 async cleanUp() { 59 if (!this._initialized) { 60 throw new Components.Exception( 61 "Directory not initialized", 62 Cr.NS_ERROR_NOT_INITIALIZED 63 ); 64 } 65 } 66 67 get _prefBranch() { 68 if (this.__prefBranch) { 69 return this.__prefBranch; 70 } 71 if (!this._dirPrefId) { 72 throw Components.Exception("No dirPrefId!", Cr.NS_ERROR_NOT_AVAILABLE); 73 } 74 return (this.__prefBranch = Services.prefs.getBranch( 75 `${this._dirPrefId}.` 76 )); 77 } 78 /** @abstract */ 79 get lists() { 80 throw new Components.Exception( 81 `${this.constructor.name} does not implement lists getter.`, 82 Cr.NS_ERROR_NOT_IMPLEMENTED 83 ); 84 } 85 /** @abstract */ 86 get cards() { 87 throw new Components.Exception( 88 `${this.constructor.name} does not implement cards getter.`, 89 Cr.NS_ERROR_NOT_IMPLEMENTED 90 ); 91 } 92 93 getCard(uid) { 94 let card = new AddrBookCard(); 95 card.directoryUID = this.UID; 96 card._uid = uid; 97 card._properties = this.loadCardProperties(uid); 98 return card.QueryInterface(Ci.nsIAbCard); 99 } 100 /** @abstract */ 101 loadCardProperties(uid) { 102 throw new Components.Exception( 103 `${this.constructor.name} does not implement loadCardProperties.`, 104 Cr.NS_ERROR_NOT_IMPLEMENTED 105 ); 106 } 107 /** @abstract */ 108 saveCardProperties(card) { 109 throw new Components.Exception( 110 `${this.constructor.name} does not implement saveCardProperties.`, 111 Cr.NS_ERROR_NOT_IMPLEMENTED 112 ); 113 } 114 /** @abstract */ 115 deleteCard(uid) { 116 throw new Components.Exception( 117 `${this.constructor.name} does not implement deleteCard.`, 118 Cr.NS_ERROR_NOT_IMPLEMENTED 119 ); 120 } 121 /** @abstract */ 122 saveList(list) { 123 throw new Components.Exception( 124 `${this.constructor.name} does not implement saveList.`, 125 Cr.NS_ERROR_NOT_IMPLEMENTED 126 ); 127 } 128 /** @abstract */ 129 deleteList(uid) { 130 throw new Components.Exception( 131 `${this.constructor.name} does not implement deleteList.`, 132 Cr.NS_ERROR_NOT_IMPLEMENTED 133 ); 134 } 135 136 /* nsIAbDirectory */ 137 138 get readOnly() { 139 return this._readOnly; 140 } 141 get isRemote() { 142 return false; 143 } 144 get isSecure() { 145 return false; 146 } 147 get propertiesChromeURI() { 148 return "chrome://messenger/content/addressbook/abAddressBookNameDialog.xhtml"; 149 } 150 get dirPrefId() { 151 return this._dirPrefId; 152 } 153 get dirName() { 154 if (this._dirName === null) { 155 this._dirName = this.getLocalizedStringValue("description", ""); 156 } 157 return this._dirName; 158 } 159 set dirName(value) { 160 this.setLocalizedStringValue("description", value); 161 this._dirName = value; 162 Services.obs.notifyObservers(this, "addrbook-directory-updated", "DirName"); 163 } 164 get dirType() { 165 return Ci.nsIAbManager.JS_DIRECTORY_TYPE; 166 } 167 get fileName() { 168 return this._fileName; 169 } 170 get UID() { 171 if (!this._uid) { 172 if (this._prefBranch.getPrefType("uid") == Services.prefs.PREF_STRING) { 173 this._uid = this._prefBranch.getStringPref("uid"); 174 } else { 175 this._uid = newUID(); 176 this._prefBranch.setStringPref("uid", this._uid); 177 } 178 } 179 return this._uid; 180 } 181 get URI() { 182 return this._uri; 183 } 184 get position() { 185 return this._prefBranch.getIntPref("position", 1); 186 } 187 get childNodes() { 188 let lists = Array.from( 189 this.lists.values(), 190 list => 191 new AddrBookMailingList( 192 list.uid, 193 this, 194 list.name, 195 list.nickName, 196 list.description 197 ).asDirectory 198 ); 199 lists.sort(compareAddressBooks); 200 return lists; 201 } 202 get childCards() { 203 let results = Array.from( 204 this.lists.values(), 205 list => 206 new AddrBookMailingList( 207 list.uid, 208 this, 209 list.name, 210 list.nickName, 211 list.description 212 ).asCard 213 ).concat(Array.from(this.cards.keys(), this.getCard, this)); 214 215 return results; 216 } 217 get supportsMailingLists() { 218 return true; 219 } 220 221 search(query, string, listener) { 222 if (!listener) { 223 return; 224 } 225 if (!query) { 226 listener.onSearchFinished(Cr.NS_ERROR_FAILURE, true, null, ""); 227 return; 228 } 229 if (query[0] == "?") { 230 query = query.substring(1); 231 } 232 233 let results = Array.from( 234 this.lists.values(), 235 list => 236 new AddrBookMailingList( 237 list.uid, 238 this, 239 list.name, 240 list.nickName, 241 list.description 242 ).asCard 243 ).concat(Array.from(this.cards.keys(), this.getCard, this)); 244 245 // Process the query string into a tree of conditions to match. 246 let lispRegexp = /^\((and|or|not|([^\)]*)(\)+))/; 247 let index = 0; 248 let rootQuery = { children: [], op: "or" }; 249 let currentQuery = rootQuery; 250 251 while (true) { 252 let match = lispRegexp.exec(query.substring(index)); 253 if (!match) { 254 break; 255 } 256 index += match[0].length; 257 258 if (["and", "or", "not"].includes(match[1])) { 259 // For the opening bracket, step down a level. 260 let child = { 261 parent: currentQuery, 262 children: [], 263 op: match[1], 264 }; 265 currentQuery.children.push(child); 266 currentQuery = child; 267 } else { 268 let [name, condition, value] = match[2].split(","); 269 currentQuery.children.push({ 270 name, 271 condition, 272 value: decodeURIComponent(value).toLowerCase(), 273 }); 274 275 // For each closing bracket except the first, step up a level. 276 for (let i = match[3].length - 1; i > 0; i--) { 277 currentQuery = currentQuery.parent; 278 } 279 } 280 } 281 282 results = results.filter(card => { 283 let properties; 284 if (card.isMailList) { 285 properties = new Map([ 286 ["DisplayName", card.displayName], 287 ["NickName", card.getProperty("NickName", "")], 288 ["Notes", card.getProperty("Notes", "")], 289 ]); 290 } else { 291 properties = card._properties; 292 } 293 let matches = b => { 294 if ("condition" in b) { 295 let { name, condition, value } = b; 296 if (name == "IsMailList" && condition == "=") { 297 return card.isMailList == (value == "true"); 298 } 299 let cardValue = properties.get(name); 300 if (!cardValue) { 301 return condition == "!ex"; 302 } 303 if (condition == "ex") { 304 return true; 305 } 306 307 cardValue = cardValue.toLowerCase(); 308 switch (condition) { 309 case "=": 310 return cardValue == value; 311 case "!=": 312 return cardValue != value; 313 case "lt": 314 return cardValue < value; 315 case "gt": 316 return cardValue > value; 317 case "bw": 318 return cardValue.startsWith(value); 319 case "ew": 320 return cardValue.endsWith(value); 321 case "c": 322 return cardValue.includes(value); 323 case "!c": 324 return !cardValue.includes(value); 325 case "~=": 326 case "regex": 327 default: 328 return false; 329 } 330 } 331 if (b.op == "or") { 332 return b.children.some(bb => matches(bb)); 333 } 334 if (b.op == "and") { 335 return b.children.every(bb => matches(bb)); 336 } 337 if (b.op == "not") { 338 return !matches(b.children[0]); 339 } 340 return false; 341 }; 342 343 return matches(rootQuery); 344 }, this); 345 346 for (let card of results) { 347 listener.onSearchFoundCard(card); 348 } 349 listener.onSearchFinished(Cr.NS_OK, true, null, ""); 350 } 351 generateName(generateFormat, bundle) { 352 return this.dirName; 353 } 354 cardForEmailAddress(emailAddress) { 355 return ( 356 this.getCardFromProperty("PrimaryEmail", emailAddress, false) || 357 this.getCardFromProperty("SecondEmail", emailAddress, false) 358 ); 359 } 360 /** @abstract */ 361 getCardFromProperty(property, value, caseSensitive) { 362 throw new Components.Exception( 363 `${this.constructor.name} does not implement getCardFromProperty.`, 364 Cr.NS_ERROR_NOT_IMPLEMENTED 365 ); 366 } 367 /** @abstract */ 368 getCardsFromProperty(property, value, caseSensitive) { 369 throw new Components.Exception( 370 `${this.constructor.name} does not implement getCardsFromProperty.`, 371 Cr.NS_ERROR_NOT_IMPLEMENTED 372 ); 373 } 374 getMailListFromName(name) { 375 for (let list of this.lists.values()) { 376 if (list.name.toLowerCase() == name.toLowerCase()) { 377 return new AddrBookMailingList( 378 list.uid, 379 this, 380 list.name, 381 list.nickName, 382 list.description 383 ).asDirectory; 384 } 385 } 386 return null; 387 } 388 deleteDirectory(directory) { 389 if (this._readOnly) { 390 throw new Components.Exception( 391 "Directory is read-only", 392 Cr.NS_ERROR_FAILURE 393 ); 394 } 395 396 let list = this.lists.get(directory.UID); 397 list = new AddrBookMailingList( 398 list.uid, 399 this, 400 list.name, 401 list.nickName, 402 list.description 403 ); 404 405 this.deleteList(directory.UID); 406 407 Services.obs.notifyObservers( 408 list.asDirectory, 409 "addrbook-list-deleted", 410 this.UID 411 ); 412 } 413 hasCard(card) { 414 return this.lists.has(card.UID) || this.cards.has(card.UID); 415 } 416 hasDirectory(dir) { 417 return this.lists.has(dir.UID); 418 } 419 hasMailListWithName(name) { 420 return this.getMailListFromName(name) != null; 421 } 422 addCard(card) { 423 return this.dropCard(card, false); 424 } 425 modifyCard(card) { 426 if (this._readOnly && !this._overrideReadOnly) { 427 throw new Components.Exception( 428 "Directory is read-only", 429 Cr.NS_ERROR_FAILURE 430 ); 431 } 432 433 let oldProperties = this.loadCardProperties(card.UID); 434 let changedProperties = new Set(oldProperties.keys()); 435 436 for (let { name, value } of card.properties) { 437 if (!oldProperties.has(name) && ![null, undefined, ""].includes(value)) { 438 changedProperties.add(name); 439 } else if (oldProperties.get(name) == value) { 440 changedProperties.delete(name); 441 } 442 } 443 changedProperties.delete("LastModifiedDate"); 444 445 this.saveCardProperties(card); 446 447 if (changedProperties.size == 0) { 448 return; 449 } 450 451 // Send the card as it is in this directory, not as passed to this function. 452 card = this.getCard(card.UID); 453 Services.obs.notifyObservers(card, "addrbook-contact-updated", this.UID); 454 455 let data = {}; 456 457 for (let name of changedProperties) { 458 data[name] = { 459 oldValue: oldProperties.get(name) || null, 460 newValue: card.getProperty(name, null), 461 }; 462 } 463 464 Services.obs.notifyObservers( 465 card, 466 "addrbook-contact-properties-updated", 467 JSON.stringify(data) 468 ); 469 } 470 deleteCards(cards) { 471 if (this._readOnly && !this._overrideReadOnly) { 472 throw new Components.Exception( 473 "Directory is read-only", 474 Cr.NS_ERROR_FAILURE 475 ); 476 } 477 478 if (cards === null) { 479 throw Components.Exception("", Cr.NS_ERROR_INVALID_POINTER); 480 } 481 482 for (let card of cards) { 483 this.deleteCard(card.UID); 484 if (this.hasOwnProperty("cards")) { 485 this.cards.delete(card.UID); 486 } 487 } 488 489 for (let card of cards) { 490 Services.obs.notifyObservers(card, "addrbook-contact-deleted", this.UID); 491 card.directoryUID = null; 492 } 493 494 // We could just delete all non-existent cards from list_cards, but a 495 // notification should be fired for each one. Let the list handle that. 496 for (let list of this.childNodes) { 497 list.deleteCards(cards); 498 } 499 } 500 dropCard(card, needToCopyCard) { 501 if (this._readOnly && !this._overrideReadOnly) { 502 throw new Components.Exception( 503 "Directory is read-only", 504 Cr.NS_ERROR_FAILURE 505 ); 506 } 507 508 if (!card.UID) { 509 throw new Error("Card must have a UID to be added to this directory."); 510 } 511 512 let newCard = new AddrBookCard(); 513 newCard.directoryUID = this.UID; 514 newCard._uid = needToCopyCard ? newUID() : card.UID; 515 516 if (this.hasOwnProperty("cards")) { 517 this.cards.set(newCard._uid, new Map()); 518 } 519 520 for (let { name, value } of card.properties) { 521 if ( 522 [ 523 "DbRowID", 524 "LowercasePrimaryEmail", 525 "LowercaseSecondEmail", 526 "RecordKey", 527 "UID", 528 ].includes(name) 529 ) { 530 // These properties are either stored elsewhere (DbRowID, UID), or no 531 // longer needed. Don't store them. 532 continue; 533 } 534 if (card.directoryUID && ["_etag", "_href"].includes(name)) { 535 // These properties belong to a different directory. Don't keep them. 536 continue; 537 } 538 newCard.setProperty(name, value); 539 } 540 this.saveCardProperties(newCard); 541 542 Services.obs.notifyObservers(newCard, "addrbook-contact-created", this.UID); 543 544 return newCard; 545 } 546 useForAutocomplete(identityKey) { 547 return ( 548 Services.prefs.getBoolPref("mail.enable_autocomplete") && 549 this.getBoolValue("enable_autocomplete", true) 550 ); 551 } 552 addMailList(list) { 553 if (this._readOnly) { 554 throw new Components.Exception( 555 "Directory is read-only", 556 Cr.NS_ERROR_FAILURE 557 ); 558 } 559 560 if (!list.isMailList) { 561 throw Components.Exception( 562 "Can't add; not a mail list", 563 Cr.NS_ERROR_UNEXPECTED 564 ); 565 } 566 567 // Check if the new name is empty. 568 if (!list.dirName) { 569 throw new Components.Exception( 570 `Mail list name must be set; list.dirName=${list.dirName}`, 571 Cr.NS_ERROR_ILLEGAL_VALUE 572 ); 573 } 574 575 // Check if the new name contains 2 spaces. 576 if (list.dirName.match(" ")) { 577 throw new Components.Exception( 578 `Invalid mail list name: ${list.dirName}`, 579 Cr.NS_ERROR_ILLEGAL_VALUE 580 ); 581 } 582 583 // Check if the new name contains the following special characters. 584 for (let char of ',;"<>') { 585 if (list.dirName.includes(char)) { 586 throw new Components.Exception( 587 `Invalid mail list name: ${list.dirName}`, 588 Cr.NS_ERROR_ILLEGAL_VALUE 589 ); 590 } 591 } 592 593 let newList = new AddrBookMailingList( 594 newUID(), 595 this, 596 list.dirName || "", 597 list.listNickName || "", 598 list.description || "" 599 ); 600 this.saveList(newList); 601 602 let newListDirectory = newList.asDirectory; 603 Services.obs.notifyObservers( 604 newListDirectory, 605 "addrbook-list-created", 606 this.UID 607 ); 608 return newListDirectory; 609 } 610 editMailListToDatabase(listCard) { 611 // Deliberately not implemented, this isn't a mailing list. 612 throw Components.Exception( 613 "editMailListToDatabase not relevant here", 614 Cr.NS_ERROR_NOT_IMPLEMENTED 615 ); 616 } 617 copyMailList(srcList) { 618 // Deliberately not implemented, this isn't a mailing list. 619 throw Components.Exception( 620 "copyMailList not relevant here", 621 Cr.NS_ERROR_NOT_IMPLEMENTED 622 ); 623 } 624 getIntValue(name, defaultValue) { 625 return this._prefBranch 626 ? this._prefBranch.getIntPref(name, defaultValue) 627 : defaultValue; 628 } 629 getBoolValue(name, defaultValue) { 630 return this._prefBranch 631 ? this._prefBranch.getBoolPref(name, defaultValue) 632 : defaultValue; 633 } 634 getStringValue(name, defaultValue) { 635 return this._prefBranch 636 ? this._prefBranch.getStringPref(name, defaultValue) 637 : defaultValue; 638 } 639 getLocalizedStringValue(name, defaultValue) { 640 if (!this._prefBranch) { 641 return defaultValue; 642 } 643 if (this._prefBranch.getPrefType(name) == Ci.nsIPrefBranch.PREF_INVALID) { 644 return defaultValue; 645 } 646 try { 647 return this._prefBranch.getComplexValue(name, Ci.nsIPrefLocalizedString) 648 .data; 649 } catch (e) { 650 // getComplexValue doesn't work with autoconfig. 651 return this._prefBranch.getStringPref(name); 652 } 653 } 654 setIntValue(name, value) { 655 this._prefBranch.setIntPref(name, value); 656 } 657 setBoolValue(name, value) { 658 this._prefBranch.setBoolPref(name, value); 659 } 660 setStringValue(name, value) { 661 this._prefBranch.setStringPref(name, value); 662 } 663 setLocalizedStringValue(name, value) { 664 let valueLocal = Cc["@mozilla.org/pref-localizedstring;1"].createInstance( 665 Ci.nsIPrefLocalizedString 666 ); 667 valueLocal.data = value; 668 this._prefBranch.setComplexValue( 669 name, 670 Ci.nsIPrefLocalizedString, 671 valueLocal 672 ); 673 } 674} 675