1 /* ***** BEGIN LICENSE BLOCK ***** 2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 3 * 4 * The contents of this file are subject to the Mozilla Public License Version 5 * 1.1 (the "License"); you may not use this file except in compliance with 6 * the License. You may obtain a copy of the License at 7 * http://www.mozilla.org/MPL/ 8 * 9 * Software distributed under the License is distributed on an "AS IS" basis, 10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 11 * for the specific language governing rights and limitations under the 12 * License. 13 * 14 * The Original Code is gContactSync. 15 * 16 * The Initial Developer of the Original Code is 17 * Josh Geenen <gcontactsync@pirules.org>. 18 * Portions created by the Initial Developer are Copyright (C) 2010 19 * the Initial Developer. All Rights Reserved. 20 * 21 * Contributor(s): 22 * 23 * Alternatively, the contents of this file may be used under the terms of 24 * either the GNU General Public License Version 2 or later (the "GPL"), or 25 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 26 * in which case the provisions of the GPL or the LGPL are applicable instead 27 * of those above. If you wish to allow use of your version of this file only 28 * under the terms of either the GPL or the LGPL, and not to allow others to 29 * use your version of this file under the terms of the MPL, indicate your 30 * decision by deleting the provisions above and replace them with the notice 31 * and other provisions required by the GPL or the LGPL. If you do not delete 32 * the provisions above, a recipient may use your version of this file under 33 * the terms of any one of the MPL, the GPL or the LGPL. 34 * 35 * ***** END LICENSE BLOCK ***** */ 36 37 if (!com) var com = {}; // A generic wrapper variable 38 // A wrapper for all GCS functions and variables 39 if (!com.gContactSync) com.gContactSync = {}; 40 41 /** 42 * This class is used to import contacts using OAuth. 43 * This requires some interaction with a remote website (pirules.org) for 44 * authentication. 45 * 46 * pirules.org stores the following information for each source 47 * - oauth_consumer_key 48 * - oauth_consumer_secret 49 * - base API URL 50 * - @me/@self URL 51 * - @me/@all or @me/@friends URL 52 * etc. 53 * It also reorganizes and signs the parameters. 54 * 55 * TODO List: 56 * - For certain sources attempt to get more information 57 * - Add error detection and handling 58 * - Use HttpRequest 59 * 60 * @class 61 */ 62 com.gContactSync.Import = { 63 /** The 'source' from which contacts are imported (Plaxo, Google, etc.) */ 64 mSource: "", 65 /** This is used internally to track whether an import is in progress */ 66 mStarted: false, 67 /** A reference to the window TODO remove */ 68 mWindow: {}, 69 /** Map for Plaxo only */ 70 mMapplaxo: { 71 /** The user's ID */ 72 id: "PlaxoID", 73 /** An array of the user's photos */ 74 photos: "PlaxoPhotos" 75 }, 76 /** Map for MySpace only */ 77 mMapmyspace: { 78 /** The user's MySpace ID */ 79 id: "MySpaceID", 80 /** 81 * The 'nickname' (MySpace only). This is mapped w/ DisplayName because it 82 * is basically all that MySpace gives. 83 */ 84 nickname: "DisplayName", 85 /** The user's thumbnail */ 86 thumbnailUrl: "MySpaceThumbnail", 87 /** The URL to the contact's profile */ 88 profileUrl: "WebPage2" 89 }, 90 /** Map for Facebook only */ 91 mMapfacebook: { 92 /** Name is a simple attribute */ 93 name: "DisplayName", 94 /** ID is also a simple attribute */ 95 id: "FacebookID", 96 /** A link to the user's Facebook profile */ 97 link: "WebPage1", 98 /** A link to the user's website */ 99 website: "WebPage2", 100 /** The user's 'About' text */ 101 about: "Notes", 102 /** The user's public profile photo */ 103 //picture: "FacebookProfilePhoto", 104 /** The user's hometown */ 105 hometown: { 106 /** The name of the contact's hometown */ 107 name: "Hometown" 108 }, 109 /** The contact's current location */ 110 location: { 111 /** The name of the contact's current location */ 112 name: "Location" 113 }, 114 /** An array of a user's job history */ 115 work: { 116 /** The most recent job */ 117 0: "", 118 /** The second most recent job */ 119 1: "Second", 120 /** The third most recent job */ 121 2: "Third", 122 /** Employer information (name and Facebook ID) */ 123 employer: { 124 /** The name of the company */ 125 name: "Company" 126 }, 127 /** Contact's position in the company */ 128 position: { 129 /** The name of the contact's position in the company */ 130 name: "JobTitle" 131 }, 132 /** The date when the contact started working for the company */ 133 start_date: "WorkStartDate", 134 /** The date when the contact stopped working for the company */ 135 end_date: "WorkEndDate" 136 } 137 }, 138 /** Maps Portable Contacts attributes to TB nsIAbCard attributes */ 139 mMap: { 140 /** name is complex */ 141 name: { 142 /** The given name for a contact */ 143 givenName: "FirstName", 144 /** The contact's last name */ 145 familyName: "LastName", 146 /** A contact's formatted name */ 147 formatted: "DisplayName", 148 /** A contact's display name */ 149 displayName: "DisplayName" 150 }, 151 /** The gender of the contact */ 152 gender: "Gender", 153 /** The contact's first (given) name */ 154 first_name: "FirstName", 155 /** The contact's last (family) name */ 156 last_name: "LastName", 157 /** A contact's display name */ 158 displayName: "DisplayName", 159 /** The contact's nickname (alias) */ 160 nickName: "NickName", 161 /** emails is an array of a contact's e-mail addresses */ 162 emails: { 163 /** The prefix for the first e-mail address */ 164 0: "Primary", 165 /** The prefix for the second e-mail address */ 166 1: "Secondary", 167 /** The prefix for the third e-mail address */ 168 2: "Third", 169 /** The prefix for the fourth e-mail address */ 170 3: "Fourth", 171 /** The prefix for the fifth e-mail address */ 172 4: "Fifth", 173 /** The suffix for an e-mail address */ 174 value: "Email", 175 /** The suffix for an e-mail's type (work, home, etc.) */ 176 type: "EmailType" 177 }, 178 /** 179 * phoneNumbers is an array of a contact's phone numbers in the form: 180 * {"type":"Home","value":"(123) 456-7890"} 181 */ 182 phoneNumbers: { 183 0: "Work", 184 1: "Home", 185 2: "Fax", 186 3: "Cell", 187 4: "Pager", 188 value: "Phone", // note that TB is inconsistent here 189 // {Home|Work}Phone and {Fax|Cellular|Pager}Number 190 type: "PhoneType" 191 }, 192 /** 193 * addresses is an array of a contact's postal addresses in the form: 194 * {"type":"Home","formatted":"1234 Main St"} 195 */ 196 addresses: { 197 0: "", 198 1: "", 199 2: "", 200 type: "", 201 formatted: "<type>Address" 202 }, 203 /** 204 * Links to a user's websites. 205 */ 206 urls: { 207 0: "WebPage1", 208 1: "WebPage2", 209 type: "Type", 210 value: "" 211 } 212 }, 213 /** Commands to execute when offline during an HTTP Request */ 214 mOfflineFunction: function Import_offlineFunc(httpReq) { 215 com.gContactSync.alertError(com.gContactSync.StringBundle.getStr('importOffline')); 216 com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('offlineImportStatusText')); 217 }, 218 /** 219 * Stores <em>encoded</em> OAuth variables, such as the oauth_token, 220 * oauth_token_secret, and oauth_verifier 221 */ 222 mOAuth: { 223 /** The OAuth token to use in requests */ 224 oauth_token: "", 225 /** The OAuth token secret to use in signing request parameters */ 226 oauth_token_secret: "", 227 /** The OAuth verifier for OAuth version 1.0a */ 228 oauth_verifier: "", 229 /** The access token (OAuth version 2.0) */ 230 access_token: "", 231 /** The expiration time (OAuth version 2.0) */ 232 expires: "" 233 }, 234 /** 235 * Step 1: Get an initial, unauthorized oauth_token and oauth_token_secret. 236 * This is done mostly on pirules.org which contains the consumer token and 237 * secret for various sources and signs the parameters. 238 * pirules.org returns the response from the source, usually of the form: 239 * oauth_token=1234&oauth_token_secret=5678 240 * 241 * @param aSource {string} The source from which the contacts are obtained, 242 * in lowercase, as supported by pirules.org. 243 */ 244 step1: function Import_step1(aSource) { 245 var imp = com.gContactSync.Import, 246 callback = aSource == "facebook" ? imp.step2b : imp.step2a; 247 if (imp.mStarted) { 248 // TODO warn the user and allow him or her to cancel 249 } 250 com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('startingImport')); 251 imp.mStarted = true; 252 imp.mSource = aSource; 253 // get an oauth_token and oauth_token_secret 254 imp.openBrowserWindow("http://www.pirules.org/oauth/index2.php?quiet&step=1&source=" + 255 imp.mSource, 256 callback); 257 }, 258 /** 259 * Step 2a: The first of two substeps where the user is prompted for his or 260 * her credentials on the third-party website. 261 * In this substep, gContactSync gets the login URL from pirules.org with 262 * all it's parameters and the oauth_signature. 263 * This is done in step 1 for OAuth 2.0 (Facebook only at the moment). 264 */ 265 step2a: function Import_step2a() { 266 var imp = com.gContactSync.Import, 267 win = imp.mWindow, 268 response = win.document ? win.document.getElementById("response") : null; 269 if (!response) { 270 com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importFailed')); 271 com.gContactSync.LOGGER.LOG_ERROR("***Import failed to get the auth tokens"); 272 com.gContactSync.LOGGER.LOG(win.document ? win.document.innerHTML : "window has no document"); 273 return; 274 } 275 response = response.innerHTML; 276 com.gContactSync.LOGGER.LOG("***IMPORT: Step 1 finished: " + win.location + 277 "\nContents:\n" + response); 278 // parse and store the parameters from step 1 (oauth_token & 279 // oauth_token_secret) 280 imp.storeResponse(response); 281 // TODO use HttpRequest 282 imp.openBrowserWindow("http://www.pirules.org/oauth/index2.php?quiet&step=2&source=" + 283 imp.mSource + 284 "&oauth_token=" + imp.mOAuth.oauth_token + 285 "&oauth_token_secret=" + imp.mOAuth.oauth_token_secret, 286 imp.step2b); 287 }, 288 /** 289 * Step 2a: The second of two substeps where the user is prompted for his or 290 * her credentials on the third-party website. 291 * In this substep, gContactSync opens a browser to the login page for the 292 * particular source. 293 */ 294 step2b: function Import_step2b() { 295 var imp = com.gContactSync.Import, 296 win = imp.mWindow, 297 response = win.document ? win.document.getElementById("response") : null; 298 if (!response) { 299 com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importFailed')); 300 com.gContactSync.LOGGER.LOG_ERROR("***Import failed to get the login URL"); 301 com.gContactSync.LOGGER.LOG(win.document ? win.document.innerHTML : "window has no document"); 302 return; 303 } 304 response = String(response.innerHTML).replace(/\&\;/g, "&"); 305 com.gContactSync.LOGGER.LOG("***IMPORT: Step 2a finished: " + win.location + "\nContents:\n" + response); 306 com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importRequestingCredentials')); 307 imp.openBrowserWindow(response, imp.logStep2b); 308 }, 309 /** 310 * Step 2b: The second of two substeps where the user is prompted for his or 311 * her credentials on the third-party website. 312 * This just logs that step 2b has finished (the login page was opened) 313 */ 314 logStep2b: function Import_logStep2b() { 315 var win = com.gContactSync.Import.mWindow; 316 com.gContactSync.LOGGER.LOG("***IMPORT: Step 2b finished: " + win.location + 317 "Please click Finish Import to continue"); 318 }, 319 /** 320 * Step 3: Gets the new oauth_token then activates the token. 321 * This step must be initiated by the user (for now). 322 * TODO - find a way to automatically start step3 when possible. 323 */ 324 step3: function Import_step3() { 325 var imp = com.gContactSync.Import; 326 if (!imp.mStarted) { 327 com.gContactSync.alertError(com.gContactSync.StringBundle.getStr("importNotStarted")); 328 return; 329 } 330 // Get the new oauth_token from the window. 331 imp.mOAuth.oauth_token = encodeURIComponent(imp.mWindow.document.getElementById('response').innerHTML); 332 imp.mWindow.close(); 333 if (!imp.mOAuth.oauth_token) { 334 com.gContactSync.alert(com.gContactSync.StringBundle.getStr('importCanceled'), 335 com.gContactSync.StringBundle.getStr('importCanceledTitle'), 336 window); 337 imp.mStarted = false; 338 return; 339 } 340 com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importActivatingToken')); 341 // activate the token 342 // TODO use HttpRequest 343 imp.openBrowserWindow("http://www.pirules.org/oauth/index2.php?quiet&step=3&source=" + 344 imp.mSource + 345 "&oauth_token=" + imp.mOAuth.oauth_token + 346 "&oauth_token_secret=" + imp.mOAuth.oauth_token_secret, 347 imp.step4); 348 }, 349 /** 350 * Step 4: Use the token to fetch the user's contacts. 351 * This sends a request and the token/token secret to pirules.org which 352 * signs and sends the request to the source's @me/@friend URL. 353 */ 354 step4: function Import_step4() { 355 var imp = com.gContactSync.Import, 356 win = imp.mWindow, 357 response = win.document ? win.document.getElementById("response") : null; 358 if (!response) { 359 com.gContactSync.LOGGER.LOG("***Import failed on step 3"); 360 return; 361 } 362 response = response.innerHTML; 363 com.gContactSync.LOGGER.LOG("***IMPORT: Step 3 finished: " + win.location + "\nContents:\n" + response); 364 imp.storeResponse(response); 365 com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importRetrievingContacts')); 366 // Use the token to fetch the user's contacts 367 // TODO use HttpRequest 368 // access_token is used instead of the oauth_token in OAuth 2.0 369 if (imp.mOAuth.access_token) { 370 imp.openBrowserWindow("http://www.pirules.org/oauth/index2.php?quiet&step=4&source=" + 371 imp.mSource + 372 "&access_token=" + imp.mOAuth.access_token, 373 imp.finish); 374 } 375 else { 376 imp.openBrowserWindow("http://www.pirules.org/oauth/index2.php?quiet&step=4&source=" + 377 imp.mSource + 378 "&oauth_token=" + imp.mOAuth.oauth_token + 379 "&oauth_token_secret=" + imp.mOAuth.oauth_token_secret, 380 imp.finish); 381 } 382 }, 383 /** 384 * Gets the response from step 4 and calls beginImport to parse the JSON feed 385 * of contacts. 386 */ 387 // Get the contact feed and import it into an AB 388 finish: function Import_finish() { 389 var imp = com.gContactSync.Import, 390 win = imp.mWindow; 391 // get the contacts feed 392 var response = win.document ? win.document.getElementById("response") : null; 393 if (!response) { 394 com.gContactSync.LOGGER.LOG("***Import failed on step 4"); 395 return; 396 } 397 response = response.innerHTML; 398 com.gContactSync.LOGGER.LOG("Final response:\n" + response); 399 imp.mStarted = false; 400 com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importParsingContacts')); 401 // start the import 402 imp.beginImport(response); 403 }, 404 /** 405 * Parses and stores a URL-encoded response in the following format: 406 * param1=value1¶m2=value2¶m3=value3... 407 * The parsed parameters and values are stored (still encoded) in 408 * com.gContactSync.Import.mOAuth[param] = value; 409 * 410 * @param aResponse {string} The encoded response to parse. 411 */ 412 storeResponse: function Import_storeResponse(aResponse) { 413 var imp = com.gContactSync.Import, 414 params = (aResponse).split("&"); 415 for (var i = 0; i < params.length; i++) { 416 var index = params[i].indexOf("="); 417 if (index > 0) { 418 var param = params[i].substr(0, index), 419 value = params[i].substr(index + 1); 420 com.gContactSync.LOGGER.VERBOSE_LOG("***" + param + "=>" + value); 421 imp.mOAuth[param] = value; 422 } 423 } 424 }, 425 /** 426 * Opens a window at the given URL and optionally sets an onbeforeunload 427 * listener. 428 * 429 * @param aUrl {string} The URL to open. 430 * @param aBeforeUnload {function} The function to run before the window is 431 * unloaded. 432 */ 433 openBrowserWindow: function Import_openBrowserWindow(aUrl, aBeforeUnload) { 434 var imp = com.gContactSync.Import; 435 com.gContactSync.LOGGER.LOG("***IMPORT: opening '" + aUrl + "'"); 436 // TODO - find a way to show a location bar, allow context menus, etc. 437 imp.mWindow = window.open(aUrl, 438 "gContactSyncImport" + aUrl, 439 "chrome=yes,location=yes,resizable=yes,height=500,width=500,modal=no"); 440 if (aBeforeUnload) { 441 imp.mWindow.onbeforeunload = aBeforeUnload; 442 } 443 }, 444 /** 445 * Begins the actual import given a JSON feed of contacts. 446 * It promps the user for a name for the destination AB (can be new or old). 447 * 448 * @param aFeed {string} The JSON feed of contacts to parse. 449 */ 450 beginImport: function Import_beginImport(aFeed) { 451 if (!aFeed) { 452 return; 453 } 454 try { 455 com.gContactSync.LOGGER.VERBOSE_LOG(aFeed); 456 var res = com.gContactSync.prompt(com.gContactSync.StringBundle.getStr("importDestination"), 457 com.gContactSync.StringBundle.getStr("importDestinationTitle"), 458 window); 459 if (!res) { 460 return; 461 } 462 var ab = new com.gContactSync.GAddressBook(com.gContactSync.GAbManager.getAbByName(res), 463 true); 464 // decode the JSON and get the array of cards 465 var nsIJSON = Components.classes["@mozilla.org/dom/json;1"] 466 .createInstance(Components.interfaces.nsIJSON); 467 try { 468 var obj = nsIJSON.decode(aFeed); 469 } 470 catch (e) { 471 com.gContactSync.alertError(aFeed); 472 return; 473 } 474 var arr = obj.entry || obj.data; 475 for (var i in arr) { 476 var contact = arr[i], 477 id = contact.id; 478 if (id) { 479 var newCard = ab.newContact(), 480 attr = ""; 481 // Download FB photos 482 if (this.mSource === "facebook") { 483 var file = com.gContactSync.writePhoto("https://graph.facebook.com/" + id + "/picture?type=large", 484 id + "_" + (new Date()).getTime()); 485 if (file) { 486 com.gContactSync.LOGGER.VERBOSE_LOG("Wrote photo...name: " + file.leafName); 487 newCard.setValue("PhotoName", file.leafName); 488 newCard.setValue("PhotoType", "file"); 489 newCard.setValue("PhotoURI", 490 Components.classes["@mozilla.org/network/io-service;1"] 491 .getService(Components.interfaces.nsIIOService) 492 .newFileURI(file) 493 .spec); 494 } 495 } 496 // Iterate through each attribute in the JSON contact 497 for (var j in contact) { 498 // If there is a map for just this source, check it for the 499 // attribute first, otherwise just use the default map. 500 if (this["mMap" + this.mSource]) 501 attr = this["mMap" + this.mSource][j] || this.mMap[j]; 502 else 503 attr = this.mMap[j]; 504 if (attr) { 505 // Download a photo of the user, if available. 506 if (j === "picture" || j === "thumbnailUrl" || j === "photos") { 507 var file = com.gContactSync.writePhoto((j === "photos" ? contact[j][0].value : contact[j]), 508 this.mSource + "_" + id, 509 0); 510 if (file) { 511 com.gContactSync.LOGGER.VERBOSE_LOG("Wrote photo...name: " + file.leafName); 512 newCard.setValue("PhotoName", file.leafName); 513 newCard.setValue("PhotoType", "file"); 514 newCard.setValue("PhotoURI", 515 Components.classes["@mozilla.org/network/io-service;1"] 516 .getService(Components.interfaces.nsIIOService) 517 .newFileURI(file) 518 .spec); 519 } 520 } 521 // when contact[j] is an Array things are a bit more 522 // complicated 523 else if (contact[j] instanceof Array) { 524 // emails: [ 525 // {email: somebody@somwhere, type: work}, 526 // {email: somebody2@somwhere, type: work} 527 // ] 528 // contact[j] = emails[] 529 // contact[j][k] = emails[k] 530 for (var k = 0; k < contact[j].length; k++) { 531 if (!attr[k]) break; 532 // contact[j][k][l] = sombody@somewhere 533 for (var l in contact[j][k]) { 534 if (attr[l]) { 535 var type = contact[j][k].type; 536 // not all arrays can be mapped to TB fields by index 537 // TODO - support using original phone # fields 538 // this would require NOT storing the type... 539 var tbAttribute = String(attr[k] + attr[l]).replace("<type>", type); 540 // Workaround for inconsistent phone number attributes in TB 541 if (attr === "phoneNumbers" && (type === "Cellular" || type === "Pager" || type === "Fax")) { 542 tbAttribute = tbAttribute.replace("Phone", "Number"); 543 } 544 // mMap[j][[k] is the prefix (Primary, Second, etc.) 545 // mMap[j][l] is the suffix (Email) 546 com.gContactSync.LOGGER.VERBOSE_LOG(" - (Array): " + tbAttribute + "=" + contact[j][k][l]); 547 newCard.setValue(tbAttribute, this.decode(contact[j][k][l])); 548 } 549 } 550 551 } 552 } 553 else if (j === "photos") { 554 // TODO download the photo... 555 // possibly implementation-specific 556 } 557 // if it is just a normal property (has a length property => 558 // string) check the map 559 else if (attr.length) { 560 com.gContactSync.LOGGER.VERBOSE_LOG(" - (String): " + attr + "=" + contact[j]) 561 newCard.setValue(attr, this.decode(contact[j])); 562 } 563 // else it is an object with subproperties 564 else { 565 for (var k in contact[j]) { 566 if (attr[k]) { 567 com.gContactSync.LOGGER.VERBOSE_LOG(" - (Object): " + attr[k] + "/" + j + "=" + contact[j][k]); 568 newCard.setValue(attr[k], this.decode(contact[j][k])); 569 } 570 } 571 } 572 } 573 } 574 newCard.update(); 575 } 576 } 577 } 578 catch (e) { 579 com.gContactSync.alertError(e); 580 return; 581 } 582 // refresh the ab results pane 583 try { 584 if (SetAbView !== undefined) { 585 SetAbView(GetSelectedDirectory(), false); 586 } 587 588 // select the first card, if any 589 if (gAbView && gAbView.getCardFromRow(0)) 590 SelectFirstCard(); 591 } 592 catch (e) {} 593 com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importFinished')); 594 com.gContactSync.alert(com.gContactSync.StringBundle.getStr("importComplete"), 595 com.gContactSync.StringBundle.getStr("importCompleteTitle"), 596 window); 597 }, 598 /** 599 * Decodes text returned in a JSON feed. 600 * @param aString {string} The text to decode. 601 * @returns {string} The decoded text. 602 */ 603 decode: function Import_decode(aString) { 604 return aString ? 605 decodeURIComponent(aString).replace(/</g, "<") 606 .replace(/>/g, ">") 607 .replace(/&/g, "&") 608 .replace(/"/g, '"') : 609 ""; 610 } 611 };