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) 2008-2009
 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 window.addEventListener("load",
 42   /** Initializes the gdata class when the window has finished loading */
 43   function gCS_gdataLoadListener(e) {
 44     com.gContactSync.gdata.contacts.init();
 45   },
 46 false);
 47 
 48 /**
 49  * Stores information on using the Google Data Api protocol, specifically the
 50  * contacts portion of the protocol.
 51  * http://code.google.com/apis/contacts/
 52  * @class
 53  */
 54 com.gContactSync.gdata = {
 55   AUTH_URL:              "https://www.google.com/accounts/ClientLogin",
 56   AUTH_REQUEST_TYPE:     "POST",
 57   AUTH_SUB_SESSION_URL:  "https://www.google.com/accounts/AuthSubSessionToken",
 58   AUTH_SUB_SESSION_TYPE: "GET",
 59   AUTH_SUB_REVOKE_URL:   "https://www.google.com/accounts/AuthSubRevokeToken",
 60   AUTH_SUB_REVOKE_TYPE:  "GET",
 61   O_AUTH_URL:            "https://www.google.com/accounts/AuthSubRequest?scope=https%3A%" +
 62                          "2F%2Fwww.google.com%2Fm8%2Ffeeds%2F&session=1&secure=0&next=" +
 63                          "http%3A%2F%2Fpirules.org%2Ftools%2Fgcs%2Findex.php",
 64   O_AUTH_TYPE:           "GET",
 65   /**
 66    * Sets up the body for an authentication request given the e-mail address
 67    * and password.
 68    * @param aEmail     {string} The user's e-mail address
 69    * @param aPassword  {string} The user's password
 70    * @returns {string} The body for an authentication request.
 71    */
 72   makeAuthBody: function gdata_makeAuthBody(aEmail, aPassword) {
 73     // NOTE: leave accountType as HOSTED_OR_GOOGLE or Google Apps for your
 74     // domain accounts won't work
 75     // fix the username (remove whitespace)
 76     aEmail = com.gContactSync.fixUsername(aEmail);
 77     return "accountType=HOSTED_OR_GOOGLE&Email=" + encodeURIComponent(aEmail) +
 78            "&Passwd=" + encodeURIComponent(aPassword) +
 79            "&service=cp&source=Josh-gContactSync-0-3";
 80   },
 81   /**
 82    * Returns the email address of the given ID.
 83    * @returns The e-mail address from an ID.
 84    */
 85   getEmailFromId: function gdata_getEmailFromId(aId) {
 86     if (!aId || !aId.indexOf || aId === "")
 87       return "";
 88     // typical ID:
 89     // http://www.google.com/m8/feeds/contacts/address%40gmail.com/base/...
 90     var startStr = "/feeds/contacts/",
 91         start    = aId.indexOf(startStr) + startStr.length,
 92         endStr   = "/base/",
 93         end      = aId.indexOf(endStr),
 94         address;
 95     if (start >= end)
 96       return "";
 97     address = decodeURIComponent(aId.substring(start, end));
 98     com.gContactSync.LOGGER.VERBOSE_LOG("found address: " + address + " from ID: " + aId);
 99     return address;
100   },
101   /** Namespaces used in the API */
102   namespaces: {
103     /** The APP namespace */
104     APP:         new com.gContactSync.Namespace("http://www.w3.org/2007/app",
105                                                 "app:"),
106     /** The ATOM namespace */
107     ATOM:        new com.gContactSync.Namespace("http://www.w3.org/2005/Atom",
108                                                 "atom:"),
109     /** The GD namespace */
110     GD:          new com.gContactSync.Namespace("http://schemas.google.com/g/2005",
111                                                 "gd:"),
112     /** The GCONTACT namespace */
113     GCONTACT:    new com.gContactSync.Namespace("http://schemas.google.com/contact/2008",
114                                                 "gContact:"),
115     /** The OPEN SEARCH namespace */
116     OPEN_SEARCH: new com.gContactSync.Namespace("http://a9.com/-/spec/opensearch/1.1/",
117                                                 "openSearch:"),
118     /** The BATCH namespace */
119     BATCH:       new com.gContactSync.Namespace("http://schemas.google.com/gdata/batch",
120                                                 "batch:")
121   },
122   /** some things related to contacts, such as related URLs and HTTP Request
123    * types
124    */
125   contacts: {
126     /** The URL to get all contacts (full) */
127     GET_ALL_URL:      "https://www.google.com/m8/feeds/contacts/default/full?" +
128                       "max-results=",
129     /** The URL to get all contacts (thin) */
130     GET_ALL_THIN_URL: "https://www.google.com/m8/feeds/contacts/default/thin?" +
131                       "max-results=",
132     /** The URL to get all groups (max 1000) */
133     GROUPS_URL:       "https://www.google.com/m8/feeds/groups/default/full?" +
134                       "max-results=1000",
135     /** The URL to add a group */
136     ADD_GROUP_URL:    "https://www.google.com/m8/feeds/groups/default/full",
137     /** The URL to add a contact */
138     ADD_URL:          "https://www.google.com/m8/feeds/contacts/default/full",
139     /** Types of relations (people somehow associated with the contact) */
140     RELATION_TYPES: {      
141       "assistant":        1,
142       "brother":          1,
143       "child":            1,
144       "domestic-partner": 1,
145       "father":           1,
146       "friend":           1,
147       "manager":          1,
148       "mother":           1,
149       "parent":           1,
150       "partner":          1,
151       "referred-by":      1,
152       "relative":         1,
153       "sister":           1,
154       "spouse":           1
155     },
156     /** Types of HTTP requests */
157     requestTypes: {
158       GET_ALL: "GET",
159       GET:     "GET",
160       UPDATE:  "PUT", // NOTE: should be set to POST and overridden
161       ADD:     "POST",
162       DELETE:  "DELETE"  // NOTE: should be set to POST and overridden
163     },
164     /** Different "types" of contact elements */
165     types: {
166       /** Has a type (#home, #work, #other, etc.) and the value is stored in a
167        * child node */
168       TYPED_WITH_CHILD: 0,
169       /** has a type and the value is stored in an attribute */
170       TYPED_WITH_ATTR: 1,
171       UNTYPED: 2,
172       /** The type is stored in the element's parent */
173       PARENT_TYPED: 3
174     },
175     /** The prefix for rel attributes */
176     rel: "http://schemas.google.com/g/2005",
177     /**
178      * Initializes the values of the tagnames with an GElement object containing
179      * information about how an Atom/XML representation of a contact from Google
180      * is stored.
181      */
182     init: function gdata_contacts_init() {
183       var GElement             = com.gContactSync.GElement,
184           untyped              = this.types.UNTYPED,
185           typedWithChild       = this.types.TYPED_WITH_CHILD,
186           typedWithAttr        = this.types.TYPED_WITH_ATTR,
187           parentTyped          = this.types.PARENT_TYPED,
188           gd                   = com.gContactSync.gdata.namespaces.GD,
189           atom                 = com.gContactSync.gdata.namespaces.ATOM,
190           gcontact             = com.gContactSync.gdata.namespaces.GCONTACT;
191       this.postalAddress       = new GElement(typedWithChild, "postalAddress",
192                                              gd, this.POSTAL_ADDRESS_TYPES);
193       this.phoneNumber         = new GElement(typedWithChild, "phoneNumber", gd,
194                                               this.PHONE_TYPES);
195       this.email               = new GElement(typedWithAttr, "email", gd,
196                                               this.EMAIL_TYPES, "address");
197       this.im                  = new GElement(typedWithAttr, "im", gd,
198                                               this.IM_TYPES, "address");
199       this.id                  = new GElement(untyped, "id", atom);
200       this.updated             = new GElement(untyped, "updated", atom);
201       this.title               = new GElement(untyped, "title", atom);
202       this.fullName            = new GElement(untyped, "fullName", gd);
203       this.givenName           = new GElement(untyped, "givenName", gd);
204       this.familyName          = new GElement(untyped, "familyName", gd);
205       this.additionalName      = new GElement(untyped, "additionalName", gd);
206       this.namePrefix          = new GElement(untyped, "namePrefix", gd);
207       this.nameSuffix          = new GElement(untyped, "nameSuffix", gd);
208       this.notes               = new GElement(untyped, "content", atom);
209       this.orgName             = new GElement(untyped, "orgName", gd);
210       this.orgTitle            = new GElement(untyped, "orgTitle", gd);
211       this.orgJobDescription   = new GElement(untyped, "orgJobDescription", gd);
212       this.orgDepartment       = new GElement(untyped, "orgDepartment", gd);
213       this.orgSymbol           = new GElement(untyped, "orgSymbol", gd);
214       this.birthday            = new GElement(untyped, "birthday", gcontact);
215       this.organization        = new GElement(typedWithAttr, "organization",
216                                               gd, ["other"]);
217       this.groupMembershipInfo = new GElement(untyped, "groupMembershipInfo",
218                                               gcontact);
219       this.relation            = new GElement(typedWithChild, "relation",
220                                               gcontact,
221                                               this.RELATION_TYPES);
222       this.nickname            = new GElement(untyped, "nickname",
223                                               gcontact);
224       this.website             = new GElement(typedWithAttr, "website",
225                                               gcontact,
226                                               this.WEBSITE_TYPES, "href");
227       this.formattedAddress    = new GElement(parentTyped, "formattedAddress", gd);
228       this.street              = new GElement(parentTyped, "street",           gd);
229       this.city                = new GElement(parentTyped, "city",             gd);
230       this.region              = new GElement(parentTyped, "region",           gd);
231       this.postcode            = new GElement(parentTyped, "postcode",         gd);
232       this.country             = new GElement(parentTyped, "country",          gd);
233     },
234     /**
235      * Updates the given element's type by setting the rel or label attribute
236      * and removing the other attribute, if present.
237      *
238      * @param aElement {XMLElement} The XML element to update.
239      * @param aType    {string}     The type to set.
240      */
241     setRelOrLabel: function gdata_setRelOrLabel(aElement, aType) {
242       if (!aElement || !aElement.setAttribute) {
243         throw "Invalid arguments passed to gdata.contacts.setRelOrLabel";
244       }
245       
246       var arr,
247           relAttr = "rel";
248       switch (aElement.tagName) {
249         case "email":
250         case "gd:email":
251           arr = this.EMAIL_TYPES;
252           break;
253         case "im":
254         case "gd:im":
255           arr = this.IM_TYPES;
256           relAttr = "protocol";
257           break;
258         case "phoneNumber":
259         case "gd:phoneNumber":
260           arr = this.PHONE_TYPES;
261           break;
262         case "structuredPostalAddress":
263         case "gd:structuredPostalAddress":
264           arr = this.POSTAL_ADDRESS_TYPES
265           break;
266         case "relation":
267         case "gd:relation":
268           arr = this.RELATION_TYPES;
269           break;
270         case "website":
271         case "gContact:website":
272           arr = this.WEBSITE_TYPES;
273           break;
274         default:
275           throw "Unrecognized tagName '" + aElement.tagName + "' in setRelOrLabel";
276       }
277       
278       // If it is NOT a custom type it should show up in arr.
279       if (!aType || (arr instanceof Array && arr.indexOf(aType) > -1 || arr[aType])) {
280       
281         // Set a rel; website and relation elements need the rel to just be the
282         // type, everything else has a prefix and im elements need a protocol.
283         if (aElement.tagName == "website" || aElement.tagName == "relation") {
284           aElement.setAttribute(relAttr, aType);
285         } else if (aElement.tagName == "im") {
286           aElement.setAttribute("protocol", com.gContactSync.gdata.contacts.rel + "#" + aType);
287           aElement.setAttribute("rel",      com.gContactSync.gdata.contacts.rel + "#other");
288         } else {
289           aElement.setAttribute(relAttr, com.gContactSync.gdata.contacts.rel + "#" + aType);
290         }
291         // Remove a label, if present
292         if (aElement.hasAttribute("label")) {
293           aElement.removeAttribute("label");
294         }
295         
296       // Otherwise it IS a custom type so it should be a label
297       } else {
298         // Set a label
299         this.mCurrentElement.setAttribute("label", aType);
300         // Remove a rel, if present
301         if (this.mCurrentElement.hasAttribute("rel")) {
302           this.mCurrentElement.removeAttribute("rel");
303         }
304       }
305     },
306     /** Different types for a website */
307     WEBSITE_TYPES: [
308       "home-page", "blog", "profile", "home", "work", "other", "ftp"
309     ],
310     /** Different types of phones */
311     PHONE_TYPES: [
312       "work", "home", "work_fax", "mobile", "pager", "home_fax", "assistant",
313       "callback", "car", "company_main", "fax", "isdn", "main", "other_fax",
314       "radio", "telex", "tty_tdd", "work_mobile", "work_pager", "other"
315     ],
316     /** Different types for IM screennames */
317     IM_TYPES: [
318       "AIM", "GOOGLE_TALK", "ICQ", "YAHOO", "MSN", "JABBER", "SKYPE", "QQ"
319     ],
320     /** E-mail address categories */
321     EMAIL_TYPES: [
322       "other", "home", "work"
323     ],
324     /** Postal address categories */
325     POSTAL_ADDRESS_TYPES: [
326       "home", "work", "other"
327     ],
328     /** Tags that are valid an an organization tag */
329     ORG_TAGS: {
330       orgDepartment:     "1",
331       orgJobDescription: "1",
332       orgName:           "1",
333       orgSymbol:         "1",
334       orgTitle:          "1"
335     },
336     /**
337      * Returns true if the given tag is valid in an organization tag
338      * @returns {boolean} True if the given tag is valid in an organization tag.
339      */
340     isOrgTag: function gdata_contacts_isOrgTag(aTagName) {
341       return this.ORG_TAGS[aTagName] ? true : false;
342     },
343     /** Valid tags in a name tag */
344     NAME_TAGS: {
345       givenName:         "1",
346       additionalName:    "1",
347       familyName:        "1",
348       namePrefix:        "1",
349       nameSuffix:        "1",
350       fullName:          "1"
351     },
352     /**
353      * Returns true if the given tag is valid in an name tag
354      * @returns {boolean} True if the given tag is valid in a name tag.
355      */
356     isNameTag: function gdata_contacts_isNameTag(aTagName) {
357       return this.NAME_TAGS[aTagName] ? true : false;
358     },
359     /** Valid tags in a structuredAddress tag */
360     ADDRESS_TAGS: {
361       housename:        "1",
362       street:           "1",
363       poBox:            "1",
364       neighborhood:     "1",
365       city:             "1",
366       subregion:        "1",
367       region:           "1",
368       postcode:         "1",
369       country:          "1",
370       formattedAddress: "1"
371     },
372     /**
373      * Returns true if the given tag is valid in a structuredAddress tag
374      * @returns {boolean} True if the given tag is valid in a structuredAddress
375      *                  tag.
376      */
377     isAddressTag: function gdata_contacts_isAddressTag(aTagName) {
378       return this.ADDRESS_TAGS[aTagName] ? true : false;
379     },
380     // different tagnames in the Atom feed, must be initialized
381     postalAddress:       {},
382     phoneNumber:         {},
383     email:               {},
384     im:                  {},
385     id:                  {},
386     updated:             {},
387     title:               {},
388     fullName:            {},
389     givenName:           {},
390     familyName:          {},
391     additionalName:      {},
392     namePrefix:          {},
393     nameSuffix:          {},
394     notes:               {},
395     orgName:             {},
396     orgTitle:            {},
397     organization:        {},
398     groupMembershipInfo: {},
399     relation:            {},
400     nickname:            {},
401     birthday:            {},
402     website:             {},
403     /** Links in the contacts feed.  The property name is the type of link
404         and the value is the value of the "rel" attribute */
405     links: {
406       /** The Photo URL */
407       PhotoURL: "http://schemas.google.com/contacts/2008/rel#photo",
408       /** The contact URL */
409       SelfURL:  "self",
410       /** The URL to edit the contact */
411       EditURL:  "edit"
412     },
413     /**
414      * Returns the total number of contacts in an Atom document.
415      * @param aXML {XML Element} The Atom feed from Google.
416      */
417     getNumberOfContacts: function gdata_contacts_getNumberOfContacts(aAtom) {
418       return aAtom.getElementsByTagNameNS("totalResults",
419                                           com.gContactSync.gdata.namespaces.OPEN_SEARCH.url);
420     }
421   },
422   /**
423    * Returns true if there is at least one auth token.
424    * @returns {boolean} True if there is at least one auth token.
425    */ 
426   isAuthValid: function gdata_isAuthValid() {
427     if (com.gContactSync.LoginManager.mNumAuthTokens === 0)
428       com.gContactSync.LoginManager.getAuthTokens();
429     return com.gContactSync.LoginManager.mNumAuthTokens > 0;
430   },
431   /**
432    * Backs up the Google contacts or groups feed to a file.
433    * @param aFeed {string}    The feed to backup (as a string).
434    * @param aAccount {string} The username of the account.
435    * @param aPrefix {string}  The prefix for the backup file.
436    * @param aSuffix {string}  The suffix for the backup file.
437    * @return {boolean} True if the backup was successful.
438    */
439   backupFeed: function gdata_backupFeed(aFeed, aAccount, aPrefix, aSuffix) {
440     var destFile = com.gContactSync.FileIO.getProfileDirectory();
441     destFile.append(com.gContactSync.FileIO.fileNames.FOLDER_NAME);
442     destFile.append(com.gContactSync.FileIO.fileNames.GOOGLE_BACKUP_DIR);
443     destFile.append((aPrefix || "") + aAccount + (aSuffix || ""));
444     com.gContactSync.LOGGER.LOG("Beginning a backup of the Google Account:\n" +
445                                 aAccount + "\nto:\n" + destFile.path);
446     return com.gContactSync.FileIO.writeToFile(destFile, aFeed);
447   }
448 };
449