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  *  - Attempt to get more contact info from MySpace
 57  * 
 58  * @class
 59  */
 60 com.gContactSync.Import = {
 61   /** The 'source' from which contacts are imported (Plaxo, Google, etc.) */
 62   mSource: "",
 63   /** This is used internally to track whether an import is in progress */
 64   mStarted: false,
 65   /** A reference to the window TODO remove */
 66   mWindow: {},
 67   /** Map for Plaxo only */
 68   mMapplaxo: {
 69     /** The user's ID */
 70     id:            "PlaxoID",
 71     /** An array of the user's photos */
 72     photos:        "PlaxoPhotos"
 73   },
 74   /** Map for MySpace only */
 75   mMapmyspace: {
 76     /** The user's MySpace ID */
 77     id:            "MySpaceID",
 78     /**
 79      * The 'nickname' (MySpace only).  This is mapped w/ DisplayName because it
 80      * is basically all that MySpace gives.
 81      */
 82     nickname:      "DisplayName",
 83     /** The user's thumbnail */
 84     thumbnailUrl:  "MySpaceThumbnail",
 85     /** The URL to the contact's profile */
 86     profileUrl:    "WebPage2"
 87   },
 88   /** Map for Facebook only */
 89   mMapfacebook: {
 90     // TODO birthday
 91     /** Name is a simple attribute */
 92     name:          "DisplayName",
 93     /** ID is also a simple attribute */
 94     id:            "FacebookID",
 95     /** A link to the user's Facebook profile */
 96     link:          "WebPage1",
 97     /** A link to the user's website */
 98     website:       "WebPage2",
 99     /** The user's 'About' text */
100     about:         "Notes",
101     /** The user's public profile photo */
102     //picture:       "FacebookProfilePhoto",
103     /** The user's hometown */
104     hometown: {
105       /** The name of the contact's hometown */
106       name: "Hometown"
107     },
108     /** The contact's current location */
109     location: {
110       /** The name of the contact's current location */
111       name: "Location"
112     },
113     /** An array of a user's job history */
114     work: {
115       /** The most recent job */
116       0: "",
117       /** The second most recent job */
118       1: "Second",
119       /** The third most recent job */
120       2: "Third",
121       /** Employer information (name and Facebook ID) */
122       employer: {
123         /** The name of the company */
124         name:      "Company"
125       },
126       /** Contact's position in the company */
127       position: {
128         /** The name of the contact's position in the company */
129         name:      "JobTitle"
130       },
131       /** The date when the contact started working for the company */
132       start_date:  "WorkStartDate",
133       /** The date when the contact stopped working for the company */
134       end_date:    "WorkEndDate"
135     }
136   },
137   /** Maps Twitter attributes to TB */
138   mMaptwitter: {
139     /** The actual name of the user */
140     name:              "DisplayName",
141     /** The screenname */
142     screen_name:       "NickName",
143     /** The internal Twitter ID */
144     id:                "TwitterID",
145     /** The user's profile image */
146     profile_image_url: "TwitterImageURL",
147     /** The user's homepage */
148     url:               "WebPage2",
149     /** The user's description */
150     description:       "Notes"
151   },
152   /** Maps Portable Contacts attributes to TB nsIAbCard attributes */
153   mMap: {
154     /** name is complex */
155     name: {
156       /** The given name for a contact */
157       givenName:   "FirstName",
158       /** The contact's last name */
159       familyName:  "LastName",
160       /** A contact's formatted name */
161       formatted:   "DisplayName",
162       /** A contact's display name */
163       displayName: "DisplayName"
164     },
165     /** The gender of the contact */
166     gender:        "Gender",
167     /** The contact's first (given) name */
168     first_name:    "FirstName",
169     /** The contact's last (family) name */
170     last_name:     "LastName",
171     /** A contact's display name */
172     displayName:   "DisplayName",
173     /** The contact's nickname (alias) */
174     nickName:      "NickName",
175     /** emails is an array of a contact's e-mail addresses */
176     emails: {
177       /** The prefix for the first e-mail address */
178       0:           "Primary",
179       /** The prefix for the second e-mail address */
180       1:           "Secondary",
181       /** The prefix for the third e-mail address */
182       2:           "Third",
183       /** The prefix for the fourth e-mail address */
184       3:           "Fourth",
185       /** The prefix for the fifth e-mail address */
186       4:           "Fifth",
187       /** The suffix for an e-mail address */
188       value:       "Email",
189       /** The suffix for an e-mail's type (work, home, etc.) */
190       type:        "EmailType"
191     },
192     /**
193      * phoneNumbers is an array of a contact's phone numbers in the form:
194      * {"type":"Home","value":"(123) 456-7890"}
195      */
196     phoneNumbers: {
197       0:           "Work",
198       1:           "Home",
199       2:           "Fax",
200       3:           "Cell",
201       4:           "Pager",
202       value:       "Phone", // note that TB is inconsistent here
203                             // {Home|Work}Phone and {Fax|Cellular|Pager}Number
204       type:        "PhoneType"
205     },
206     /**
207      * addresses is an array of a contact's postal addresses in the form:
208      * {"type":"Home","formatted":"1234 Main St"}
209      */
210     addresses: {
211       0:           "",
212       1:           "",
213       2:           "",
214       type:        "",
215       formatted:   "<type>Address"
216     },
217     /**
218      * Links to a user's websites.
219      */
220     urls: {
221       0:           "WebPage1",
222       1:           "WebPage2",
223       type:        "Type",
224       value:       ""
225     },
226     /** An array of a user's job history */
227     organizations: {
228       /** The most recent job */
229       0:           "",
230       /** The second most recent job */
231       1:           "Second",
232       /** The third most recent job */
233       2:           "Third",
234       /** The person's job title */
235       title:       "JobTitle",
236       /** The person's company */
237       name:        "Company"
238     }
239   },
240   /** Commands to execute when offline during an HTTP Request */
241   mOfflineFunction: function Import_offlineFunc(httpReq) {
242     com.gContactSync.alertError(com.gContactSync.StringBundle.getStr('importOffline'));
243     com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('offlineImportStatusText'));
244   },
245   /**
246    * Stores <em>encoded</em> OAuth variables, such as the oauth_token,
247    * oauth_token_secret, and oauth_verifier
248    */
249   mOAuth: {
250     /** The OAuth token to use in requests */
251     oauth_token:        "",
252     /** The OAuth token secret to use in signing request parameters */
253     oauth_token_secret: "",
254     /** The OAuth verifier for OAuth version 1.0a */
255     oauth_verifier:     "",
256     /** The access token (OAuth version 2.0) */
257     access_token:       "",
258     /** The expiration time (OAuth version 2.0) */
259     expires:            ""
260   },
261   /**
262    * Step 1: Get an initial, unauthorized oauth_token and oauth_token_secret.
263    * This is done mostly on pirules.org which contains the consumer token and
264    * secret for various sources and signs the parameters.
265    * pirules.org returns the response from the source, usually of the form:
266    * oauth_token=1234&oauth_token_secret=5678
267    *
268    * @param aSource {string} The source from which the contacts are obtained,
269    *                         in lowercase, as supported by pirules.org.
270    */
271   step1: function Import_step1(aSource) {
272     var imp      = com.gContactSync.Import,
273         callback = aSource == "facebook" ? imp.step2b : imp.step2a;
274     if (imp.mStarted) {
275       // TODO warn the user and allow him or her to cancel
276     }
277     
278     // Reset mOAuth
279     imp.mOAuth.oauth_token        = "";
280     imp.mOAuth.oauth_token_secret = "";
281     imp.mOAuth.oauth_verifier     = "";
282     imp.mOAuth.access_token       = "";
283     imp.mOAuth.expires            = "";
284     
285     com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('startingImport'));
286     imp.mStarted = true;
287     imp.mSource  = aSource;
288     // get an oauth_token and oauth_token_secret and give pirules.org some
289     // strings
290     imp.httpReqWrapper("http://www.pirules.org/oauth/index2.php?quiet&silent&step=1&source=" +
291                        imp.mSource +
292                        "&title=" +
293                        encodeURIComponent(com.gContactSync.StringBundle.getStr('importTitle')) +
294                        "&instructions_title=" +
295                        encodeURIComponent(com.gContactSync.StringBundle.getStr('importInstructionsTitle')) +
296                        "&instructions_0=" +
297                        encodeURIComponent(com.gContactSync.StringBundle.getStr('importInstructions0')) +
298                        "&instructions_1=" +
299                        encodeURIComponent(com.gContactSync.StringBundle.getStr('importInstructions1')),
300                        callback);
301   },
302   /**
303    * Step 2a: The first of two substeps where the user is prompted for his or
304    * her credentials on the third-party website.
305    * In this substep, gContactSync gets the login URL from pirules.org with
306    * all it's parameters and the oauth_signature.
307    * This is done in step 1 for OAuth 2.0 (Facebook only at the moment).
308    */
309   step2a: function Import_step2a(httpReq) {
310     var imp = com.gContactSync.Import,
311         response = httpReq.responseText;
312     if (!response) {
313       com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importFailed'));
314       com.gContactSync.LOGGER.LOG_ERROR("***Import failed to get the auth tokens");
315       return;
316     }
317     com.gContactSync.LOGGER.LOG("***IMPORT: Step 1 finished:\nContents:\n" +
318                                 response);
319     // parse and store the parameters from step 1 (oauth_token &
320     // oauth_token_secret)
321     imp.storeResponse(response.replace("&", "&"));
322     imp.httpReqWrapper("http://www.pirules.org/oauth/index2.php?quiet&silent&step=2&source=" +
323                        imp.mSource +
324                        "&oauth_token=" + imp.mOAuth.oauth_token +
325                        "&oauth_token_secret=" + imp.mOAuth.oauth_token_secret,
326                        imp.step2b);
327   },
328   /**
329    * Step 2b: The second of two substeps where the user is prompted for his or
330    * her credentials on the third-party website.
331    * In this substep, gContactSync opens a browser to the login page for the
332    * particular source.
333    */
334   step2b: function Import_step2b(httpReq) {
335     var imp = com.gContactSync.Import,
336         response = httpReq.responseText;
337     if (!response) {
338       com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importFailed'));
339       com.gContactSync.LOGGER.LOG_ERROR("***Import failed to get the login URL");
340       return;
341     }
342     response = String(response).replace(/\&\;/g, "&");
343     com.gContactSync.LOGGER.LOG("***IMPORT: Step 2a finished:\nContents:\n" + response);
344     com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importRequestingCredentials'));
345     imp.openBrowserWindow(response, imp.logStep2b);
346   },
347   /**
348    * Step 2b: The second of two substeps where the user is prompted for his or
349    * her credentials on the third-party website.
350    * This just logs that step 2b has finished (the login page was opened)
351    */
352   logStep2b: function Import_logStep2b() {
353     var win = com.gContactSync.Import.mWindow;
354     com.gContactSync.LOGGER.LOG("***IMPORT: Step 2b finished: " + win.location +
355                                 "Please click Finish Import to continue");
356   },
357   /**
358    * Step 3: Gets the new oauth_token then activates the token.
359    * This step must be initiated by the user (for now).
360    * TODO - find a way to automatically start step3 when possible.
361    */
362   step3: function Import_step3() {
363     var imp = com.gContactSync.Import;
364     if (!imp.mStarted) {
365       com.gContactSync.alertError(com.gContactSync.StringBundle.getStr("importNotStarted"));
366       return;
367     }
368     // Get the new oauth_token from the window.
369     imp.mOAuth.oauth_token = encodeURIComponent(imp.mWindow.document.getElementById('response').innerHTML);
370     // Get the oauth_verifier, if any
371     if (imp.mWindow.document.getElementById("oauth_verifier")) {
372       imp.mOAuth.oauth_verifier = encodeURIComponent(imp.mWindow.document.getElementById('oauth_verifier').innerHTML);
373     }
374     imp.mWindow.close();
375     if (!imp.mOAuth.oauth_token) {
376       com.gContactSync.alert(com.gContactSync.StringBundle.getStr('importCanceled'),
377                              com.gContactSync.StringBundle.getStr('importCanceledTitle'),
378                              window);
379       imp.mStarted = false;
380       return;
381     }
382     com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importActivatingToken'));
383     // activate the token
384     imp.httpReqWrapper("http://www.pirules.org/oauth/index2.php?quiet&silent&step=3&source=" +
385                          imp.mSource +
386                          "&oauth_token=" + imp.mOAuth.oauth_token +
387                          "&oauth_token_secret=" + imp.mOAuth.oauth_token_secret +
388                          (imp.mOAuth.oauth_verifier ? "&oauth_verifier=" + imp.mOAuth.oauth_verifier : ""),
389                          imp.step4);
390   },
391   /**
392    * Step 4: Use the token to fetch the user's contacts.
393    * This sends a request and the token/token secret to pirules.org which
394    * signs and sends the request to the source's @me/@friend URL.
395    */
396   step4: function Import_step4(httpReq) {
397     var imp = com.gContactSync.Import,
398         response = httpReq.responseText;
399     if (!response) {
400       com.gContactSync.LOGGER.LOG("***Import failed on step 3");
401       return;
402     }
403     com.gContactSync.LOGGER.LOG("***IMPORT: Step 3 finished:\nContents:\n" + response);
404     imp.storeResponse(response.replace("&", "&"));
405     com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importRetrievingContacts'));
406     // Use the token to fetch the user's contacts
407     // access_token is used instead of the oauth_token in OAuth 2.0
408     if (imp.mOAuth.access_token) {
409       imp.httpReqWrapper("http://www.pirules.org/oauth/index2.php?quiet&silent&step=4&source=" +
410                          imp.mSource +
411                          "&access_token=" + imp.mOAuth.access_token,
412                          imp.finish);
413     }
414     else {
415       imp.httpReqWrapper("http://www.pirules.org/oauth/index2.php?quiet&silent&step=4&source=" +
416                          imp.mSource +
417                          "&oauth_token=" + imp.mOAuth.oauth_token +
418                          "&oauth_token_secret=" + imp.mOAuth.oauth_token_secret,
419                          imp.finish);
420     }
421   },
422   /**
423    * Gets the response from step 4 and calls beginImport to parse the JSON feed
424    * of contacts.
425    */
426   // Get the contact feed and import it into an AB
427   finish: function Import_finish(httpReq) {
428     var imp = com.gContactSync.Import,
429         response = httpReq.responseText;
430     if (!response) {
431       com.gContactSync.LOGGER.LOG("***Import failed on step 4");
432       return;
433     }
434     com.gContactSync.LOGGER.LOG("Final response:\n" + response);
435     imp.mStarted = false;
436     com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importParsingContacts'));
437     // start the import
438     imp.beginImport(response);
439   },
440   /**
441    * Parses and stores a URL-encoded response in the following format:
442    * param1=value1&param2=value2&param3=value3...
443    * The parsed parameters and values are stored (still encoded) in
444    * com.gContactSync.Import.mOAuth[param] = value;
445    *
446    * @param aResponse {string} The encoded response to parse.
447    */
448   storeResponse: function Import_storeResponse(aResponse) {
449     var imp    = com.gContactSync.Import,
450         params = (aResponse).split("&");
451     for (var i = 0; i < params.length; i++) {
452       var index = params[i].indexOf("=");
453       if (index > 0) {
454         var param = params[i].substr(0, index),
455             value = params[i].substr(index + 1);
456         com.gContactSync.LOGGER.VERBOSE_LOG("***" + param + "=>" + value);
457         imp.mOAuth[param] = value;
458       }
459     }
460   },
461   /**
462    * Opens a window at the given URL and optionally sets an onbeforeunload
463    * listener.
464    *
465    * @param aUrl {string} The URL to open.
466    * @param aBeforeUnload {function} The function to run before the window is
467    *                                 unloaded.
468    */
469   openBrowserWindow: function Import_openBrowserWindow(aUrl, aBeforeUnload) {
470     var imp = com.gContactSync.Import;
471     com.gContactSync.LOGGER.LOG("***IMPORT: opening '" + aUrl + "'");
472     // TODO - find a way to show a location bar, allow context menus, etc.
473     imp.mWindow = window.open(aUrl,
474                               "gContactSyncImport" + aUrl,
475                               "chrome=yes,location=yes,resizable=yes,height=500,width=500,modal=no");
476     if (aBeforeUnload) {
477       imp.mWindow.onbeforeunload = aBeforeUnload;
478     }
479   },
480   /**
481    * Begins the actual import given a JSON feed of contacts.
482    * It promps the user for a name for the destination AB (can be new or old).
483    *
484    * @param aFeed {string} The JSON feed of contacts to parse.
485    */
486   beginImport: function Import_beginImport(aFeed) {
487     if (!aFeed) {
488       return;
489     }
490     try {
491       com.gContactSync.LOGGER.VERBOSE_LOG(aFeed);
492       var obj = aFeed;
493       // decode the JSON and get the array of cards
494       var nsIJSON = Components.classes["@mozilla.org/dom/json;1"]
495                               .createInstance(Components.interfaces.nsIJSON);
496       try {
497         obj = nsIJSON.decode(aFeed);
498       }
499       catch (e) {
500         com.gContactSync.alertError(com.gContactSync.StringBundle.getStr("importFailedMsg"));
501         com.gContactSync.LOGGER.LOG_ERROR("Import failed: ", aFeed);
502         com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importFailed'));
503         return;
504       }
505       var res = com.gContactSync.prompt(com.gContactSync.StringBundle.getStr("importDestination"),
506                                         com.gContactSync.StringBundle.getStr("importDestinationTitle"),
507                                         window);
508       if (!res) {
509         return;
510       }
511       var ab = new com.gContactSync.GAddressBook(com.gContactSync.GAbManager.getAbByName(res),
512                                                  true);
513       var arr = obj.entry || obj.data || obj;
514 
515       for (var i in arr) {
516         var contact = arr[i],
517             id = contact.id || contact.guid;
518         if (id || contact.name || contact.displayName) {
519           var newCard = ab.newContact(),
520               attr    = "";
521           // Download FB photos
522           if (this.mSource === "facebook" && id) {
523             var file = com.gContactSync.writePhoto("https://graph.facebook.com/" + id + "/picture?type=large",
524                                                    id + "_" + (new Date()).getTime());
525             if (file) {
526               com.gContactSync.LOGGER.VERBOSE_LOG("Wrote photo...name: " + file.leafName);
527               newCard.setValue("PhotoName", file.leafName);
528               newCard.setValue("PhotoType", "file");
529               newCard.setValue("PhotoURI",
530                                Components.classes["@mozilla.org/network/io-service;1"]
531                                          .getService(Components.interfaces.nsIIOService)
532                                          .newFileURI(file)
533                                          .spec);
534             }
535           }
536           // Iterate through each attribute in the JSON contact
537           for (var j in contact) {
538             // If there is a map for just this source, check it for the
539             // attribute first, otherwise just use the default map.
540             if (this["mMap" + this.mSource])
541               attr = this["mMap" + this.mSource][j] || this.mMap[j];
542             else
543               attr = this.mMap[j];
544 
545             if (attr) {
546               // Download a photo of the user, if available.
547               if (j === "picture" || j === "thumbnailUrl" || j === "photos" ||
548                   j === "profile_image_url") {
549                 var file = com.gContactSync.writePhoto((j === "photos" ? contact[j][0].value : contact[j]),
550                                                        this.mSource + "_" + id,
551                                                        0);
552                 if (file) {
553                   com.gContactSync.LOGGER.VERBOSE_LOG("Wrote photo...name: " + file.leafName);
554                   newCard.setValue("PhotoName", file.leafName);
555                   newCard.setValue("PhotoType", "file");
556                   newCard.setValue("PhotoURI",
557                                    Components.classes["@mozilla.org/network/io-service;1"]
558                                              .getService(Components.interfaces.nsIIOService)
559                                              .newFileURI(file)
560                                              .spec);
561                 }
562               }
563               // when contact[j] is an Array things are a bit more
564               // complicated
565               else if (contact[j] instanceof Array) {
566                 // emails: [
567                 //   {email: somebody@somwhere, type: work},
568                 //   {email: somebody2@somwhere, type: work}
569                 // ]
570                 // contact[j]    = emails[]
571                 // contact[j][k] = emails[k]
572                 for (var k = 0; k < contact[j].length; k++) {
573                   // quit if k is too large/shouldn't be stored
574                   if (!(k in attr)) {
575                     break;
576                   }
577                   // contact[j][k][l] = sombody@somewhere
578                   for (var l in contact[j][k]) {
579                     if (l in attr) {
580                       var type = contact[j][k].type;
581                       // not all arrays can be mapped to TB fields by index
582                       // TODO - support using original phone # fields
583                       // this would require NOT storing the type...
584                       var tbAttribute = String(attr[k] + attr[l]).replace("<type>", type);
585                       // Workaround for inconsistent phone number attributes in TB
586                       if (attr === "phoneNumbers" && (type === "Cellular" || type === "Pager" || type === "Fax")) {
587                         tbAttribute = tbAttribute.replace("Phone", "Number");
588                       }
589                       // mMap[j][[k] is the prefix (Primary, Second, etc.)
590                       // mMap[j][l] is the suffix (Email)
591                       com.gContactSync.LOGGER.VERBOSE_LOG(" - (Array): " + tbAttribute + "=" + contact[j][k][l]);
592                       newCard.setValue(tbAttribute, this.decode(contact[j][k][l]));
593                     }
594                   }
595                   
596                 }
597               }
598               else if (j === "photos") {
599                 // TODO download the photo...
600                 // possibly implementation-specific
601               }
602               // if it is just a normal property (has a length property =>
603               // string) check the map
604               else if (attr.length) {
605                 com.gContactSync.LOGGER.VERBOSE_LOG(" - (String): " + attr + "=" + contact[j])
606                 newCard.setValue(attr, this.decode(contact[j]));
607               }
608               // else it is an object with subproperties
609               else {
610                 for (var k in contact[j]) {
611                   if (k in attr) {
612                     com.gContactSync.LOGGER.VERBOSE_LOG(" - (Object): " + attr[k] + "/" + j + "=" + contact[j][k]);
613                     newCard.setValue(attr[k], this.decode(contact[j][k]));
614                   }
615                 }
616               }
617             }
618           }
619           newCard.update();
620         }
621       }
622     }
623     catch (e) {
624       com.gContactSync.alertError(e);
625       return;
626     }
627     // refresh the ab results pane
628     try {
629       if (SetAbView !== undefined) {
630         SetAbView(GetSelectedDirectory(), false);
631       }
632       
633       // select the first card, if any
634       if (gAbView && gAbView.getCardFromRow(0))
635         SelectFirstCard();
636     }
637     catch (e) {}
638     com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importFinished'));
639     com.gContactSync.alert(com.gContactSync.StringBundle.getStr("importComplete"),
640                            com.gContactSync.StringBundle.getStr("importCompleteTitle"),
641                            window);
642   },
643   /**
644    * Decodes text returned in a JSON feed.
645    * @param aString {string} The text to decode.
646    * @returns {string} The decoded text.
647    */
648   decode: function Import_decode(aString) {
649     return aString ?
650             decodeURIComponent(aString).replace(/</g,   "<")
651                                        .replace(/>/g,   ">")
652                                        .replace(/&/g,  "&")
653                                        .replace(/"/g, '"') :
654             "";
655   },
656   /**
657    * A wrapper for HttpRequest for use when importing contacts.
658    * @param aURL {string} The URL to send the GET request to.
659    * @param aCallback {function} The callback function if the request succeeds.
660    */
661   httpReqWrapper: function Import_httpReqWrapper(aURL, aCallback) {
662     var httpReq   = new com.gContactSync.HttpRequest();
663     httpReq.mUrl  = aURL;
664     httpReq.mType = "GET";
665     httpReq.mOnSuccess = aCallback;
666     httpReq.mOnOffline = this.mOfflineFunction;
667     httpReq.mOnError = function import_onError(httpReq) {
668       com.gContactSync.alertError(com.gContactSync.StringBundle.getStr("importFailedMsg"));
669       com.gContactSync.LOGGER.LOG_ERROR("Import failed: ", httpReq.responseText);
670       com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importFailed'));
671     }
672     httpReq.send();
673   },
674   /**
675    * Attempts to import from the Mozilla Labs Contacts add-on.
676    * https://wiki.mozilla.org/Labs/Contacts/ContentAPI
677    */
678   mozillaLabsContactsImporter: function Import_mozLabsImporter() {
679     if (com.gContactSync.Import.mStarted) {
680       // TODO warn the user and allow him or her to cancel
681     }
682     
683     com.gContactSync.Import.mSource = "mozLabsContacts";
684     try {
685     
686       // Import the Mozilla Labs Contacts module that loads the contacts DB
687       Components.utils.import("resource://people/modules/people.js");
688 
689       var nsIJSON = Components.classes["@mozilla.org/dom/json;1"]
690                               .createInstance(Components.interfaces.nsIJSON);
691 
692       // TODO - this needs to be much more efficient
693       var json = JSON.stringify({data: People.find({})});
694       var toEncode = {data: []};
695       var people = [];
696       
697       // decode the JSON and get the array of cards
698       try {
699         people = nsIJSON.decode(json);
700       }
701       catch (e) {
702         com.gContactSync.alertError(com.gContactSync.StringBundle.getStr("importFailedMsg"));
703         com.gContactSync.LOGGER.LOG_ERROR("Import failed: ", aFeed);
704         com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('importFailed'));
705         return;
706       }
707       
708       // Iterate through each person add add them to the JSON
709       // This loop essentially just converts the people into a portable contacts
710       // format for beginImport()
711       for (var i in people.data) {
712         var person = people.data[i].obj;
713         if (person && person.documents) {
714           var personInfo = {};
715           
716           // People can have the same info in multiple documents, this just
717           // iterates through each document and copies the details over.
718           for (var j in person.documents) {
719             for (var k in person.documents[j]) {
720               for (var l in person.documents[j][k])
721               personInfo[l] = person.documents[j][k][l];
722               com.gContactSync.LOGGER.LOG(j + "." + k + "." + l + " - " + person.documents[j][k][l])
723             }
724           }
725           toEncode.data.push(personInfo);
726         }
727       }
728       com.gContactSync.Import.beginImport(JSON.stringify(toEncode));
729     } catch (e) {
730       com.gContactSync.alertError(com.gContactSync.StringBundle.getStr("mozLabsContactsImportFailed"));
731       com.gContactSync.LOGGER.LOG_ERROR("Mozilla Labs Contacts Import Failed", e);
732     }
733   }
734 };