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 Google Calendar Provider code.
15 *
16 * The Initial Developer of the Original Code is
17 * Philipp Kewisch <mozilla@kewis.ch>
18 * Portions created by the Initial Developer are Copyright (C) 2006
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 // This constant is an arbitrary large number. It is used to tell google to get
38 // many events, the exact number is not important.
39 const kMANY_EVENTS = 0x7FFFFFFF;
40
41 /**
42 * calGoogleSession
43 * This Implements a Session object to communicate with google
44 *
45 * @constructor
46 * @class
47 */
calGoogleSession
48 function calGoogleSession(aUsername) {
49
50 this.mItemQueue = new Array();
51 this.mGoogleUser = aUsername;
52
53 var username = { value: aUsername };
54 var password = { value: null };
55
56 // Try to get the password from the password manager
57 if (passwordManagerGet(aUsername, password)) {
58 this.mGooglePass = password.value;
59 this.savePassword = true;
60 LOG("Retrieved Password for " + aUsername + " in constructor");
61 }
62 }
63
64 calGoogleSession.prototype = {
65
cGS_QueryInterface
66 QueryInterface: function cGS_QueryInterface(aIID) {
67 if (!aIID.equals(Components.interfaces.nsIInterfaceRequestor) &&
68 !aIID.equals(Components.interfaces.nsISupports)) {
69 throw Components.results.NS_ERROR_NO_INTERFACE;
70 }
71 return this;
72 },
73
74 /* Member Variables */
75 mGoogleUser: null,
76 mGooglePass: null,
77 mGoogleFullName: null,
78 mAuthToken: null,
79 mSessionID: null,
80
81 mLoggingIn: false,
82 mSavePassword: false,
83 mItemQueue: null,
84
85 mCalendarName: null,
86
87 /**
88 * readonly attribute authToken
89 *
90 * The auth token returned from Google Accounts
91 */
cGS_getAuthToken
92 get authToken cGS_getAuthToken() {
93 return this.mAuthToken;
94 },
95
96 /**
97 * attribute savePassword
98 *
99 * Sets if the password for this user should be saved or not
100 */
cGS_getSavePassword
101 get savePassword cGS_getSavePassword() {
102 return this.mSavePassword;
103 },
cGS_setSavePassword
104 set savePassword cGS_setSavePassword(v) {
105 return this.mSavePassword = v;
106 },
107
108 /**
109 * attribute googleFullName
110 *
111 * The Full Name of the user. If this is unset, it will return the
112 * this.googleUser, to ensure a non-zero value
113 */
cGS_getGoogleFullName
114 get googleFullName cGS_getGoogleFullName() {
115 return (this.mGoogleFullName ? this.mGoogleFullName :
116 this.googleUser);
117 },
cGS_setGoogleFullName
118 set googleFullName cGS_setGoogleFullName(v) {
119 return this.mGoogleFullName = v;
120 },
121
122 /**
123 * readonly attribute googleUser
124 *
125 * The username of the session. This does not necessarily have to be the
126 * email found in /calendar/feeds/email/private/full
127 */
cGS_getGoogleUser
128 get googleUser cGS_getGoogleUser() {
129 return this.mGoogleUser;
130 },
131
132 /**
133 * attribute googlePassword
134 *
135 * Sets the password used for the login process
136 */
cGS_getGooglePassword
137 get googlePassword cGS_getGooglePassword() {
138 return this.mGooglePass;
139 },
cGS_setGooglePassword
140 set googlePassword cGS_setGooglePassword(v) {
141 return this.mGooglePass = v;
142 },
143
144 /**
145 * invalidate
146 * Resets the Auth token and password.
147 */
cGS_invalidate
148 invalidate: function cGS_invalidate() {
149 this.mAuthToken = null;
150 this.mGooglePass = null;
151
152 passwordManagerRemove(this.mGoogleUser);
153 },
154
155 /**
156 * failQueue
157 * Fails all requests in this session's queue. Optionally only fail requests
158 * for a certain calendar
159 *
160 * @param aCode Failure Code
161 * @param aCalendar The calendar to fail for. Can be null.
162 */
cGS_failQueue
163 failQueue: function cGS_failQueue(aCode, aCalendar) {
164 function cGS_failQueue_failInQueue(element, index, arr) {
165 if (!aCalendar || (aCalendar && element.calendar == aCalendar)) {
166 element.fail(aCode);
167 return false;
168 }
169 return true;
170 }
171
172 this.mItemQueue = this.mItemQueue.filter(cGS_failQueue_failInQueue);
173 },
174
175 /**
176 * loginAndContinue
177 * Prepares a login request, then requests via #asyncRawRequest
178 *
179 *
180 * @param aRequest The request that initiated the login
181 */
cGS_loginAndContinue
182 loginAndContinue: function cGS_loginAndContinue(aRequest) {
183
184 if (this.mLoggingIn) {
185 LOG("loginAndContinue called while logging in");
186 return;
187 }
188 try {
189 LOG("Logging in to " + this.mGoogleUser);
190
191 // We need to have a user and should not be logging in
192 ASSERT(!this.mLoggingIn);
193 ASSERT(this.mGoogleUser);
194
195 // Check if we have a password. If not, authentication may have
196 // failed.
197 if (!this.mGooglePass) {
198 var username= { value: this.mGoogleUser };
199 var password = { value: null };
200 var savePassword = { value: false };
201
202 // Try getting a new password, potentially switching sesssions.
203 var calendarName = (aRequest.calendar ?
204 aRequest.calendar.googleCalendarName :
205 this.mGoogleUser);
206
207 if (getCalendarCredentials(calendarName,
208 username,
209 password,
210 savePassword)) {
211
212 LOG("Got the pw from the calendar credentials: " +
213 calendarName);
214
215 // If a different username was entered, switch sessions
216
217 if (aRequest.calendar &&
218 username.value != this.mGoogleUser) {
219
220 var newSession = getSessionByUsername(username.value);
221 newSession.googlePassword = password.value;
222 newSession.savePassword = savePassword.value;
223 setCalendarPref(aRequest.calendar,
224 "googleUser",
225 "CHAR",
226 username.value);
227
228 // Set the new session for the calendar
229 aRequest.calendar.session = newSession;
230 LOG("Setting " + aRequest.calendar.name +
231 "'s Session to " + newSession.googleUser);
232
233 // Move all requests by this calendar to its new session
234 function cGS_login_moveToSession(element, index, arr) {
235 if (element.calendar == aRequest.calendar) {
236 LOG("Moving " + element.uri + " to " +
237 newSession.googleUser);
238 newSession.asyncItemRequest(element);
239 return false;
240 }
241 return true;
242 }
243 this.mItemQueue = this.mItemQueue
244 .filter(cGS_login_moveToSession);
245
246 // Do not complete the request here, since it has been
247 // moved. This is not an error though, so nothing is
248 // thrown.
249 return;
250 }
251
252 // If we arrive at this point, then the session was not
253 // changed. Just adapt the password from the dialog and
254 // continue.
255 this.mGooglePass = password.value;
256 this.savePassword = savePassword.value;
257 } else {
258 LOG("Could not get any credentials for " +
259 calendarName + " (" +
260 this.mGoogleUser + ")");
261
262 // The User even canceled the login prompt asking for
263 // the user. This means we have to fail all requests
264 // that belong to that calendar and are in the queue. This
265 // will also include the request that initiated the login
266 // request, so that dosent need to be handled extra.
267 this.failQueue(Components.results.NS_ERROR_NOT_AVAILABLE,
268 aRequest.calendar);
269
270 // Unset the session in the requesting calendar, if the user
271 // canceled the login dialog that also asks for the
272 // username, then the session is not valid. This also
273 // prevents multiple login windows.
274 if (aRequest.calendar)
275 aRequest.calendar.session = null;
276
277 return;
278 }
279 }
280
281 // Now we should have a password
282 ASSERT(this.mGooglePass);
283
284 // Start logging in
285 this.mLoggingIn = true;
286
287 // Get Version info
288 var appInfo = Components.classes["@mozilla.org/xre/app-info;1"].
289 getService(Components.interfaces.nsIXULAppInfo);
290 var source = appInfo.vendor + "-" +
291 appInfo.name + "-" +
292 appInfo.version;
293
294 // Request Login
295 var request = new calGoogleRequest(this);
296
297 request.type = request.LOGIN;
298 request.extraData = aRequest;
299 request.setResponseListener(this, this.loginComplete);
300 request.setUploadData("application/x-www-form-urlencoded",
301 "Email=" + encodeURIComponent(this.mGoogleUser) +
302 "&Passwd=" + encodeURIComponent(this.mGooglePass) +
303 "&accountType=HOSTED_OR_GOOGLE" +
304 "&source=" + encodeURIComponent(source) +
305 "&service=cl");
306 this.asyncRawRequest(request);
307 } catch (e) {
308 // If something went wrong, reset the login state just in case
309 this.mLoggingIn = false;
310 LOG({action:"Error Logging In",
311 result: e.result,
312 message: e.message});
313
314 // If something went wrong, then this.loginComplete should handle
315 // the error. We don't need to take care of aRequest, since it is
316 // also in this.mItemQueue.
317 this.loginComplete(null, e.result, e.message);
318 }
319 },
320
321 /**
322 * loginComplete
323 * Callback function that is called when the login request to Google
324 * Accounts has finished
325 * - Retrieves the Authentication Token
326 * - Saves the Password in the Password Manager
327 * - Processes the Item Queue
328 *
329 * @private
330 * @param aRequest The request object that initiated the login
331 * @param aStatus The return status of the request.
332 * @param aResult The (String) Result of the Request
333 * (or an Error Message)
334 */
cGS_loginComplete
335 loginComplete: function cGS_loginComplete(aRequest, aStatus, aResult) {
336
337 // About mLoggingIn: this should only be set to false when either
338 // something went wrong or mAuthToken is set. This avoids redundant
339 // logins to Google. Hence mLoggingIn is set three times in the course
340 // of this function
341
342 if (!aResult || aStatus != Components.results.NS_OK) {
343 this.mLoggingIn = false;
344 LOG("Login failed. Status: " + aStatus);
345
346 if (aStatus == kGOOGLE_LOGIN_FAILED) {
347 // If the login failed, then retry the login. This is not an
348 // error that should trigger failing the calICalendar's request.
349 // The login request's extraData contains the request object
350 // that triggered the login initially
351 this.loginAndContinue(aRequest.extraData);
352 } else {
353 LOG("Failing queue with " + aStatus);
354 this.failQueue(aStatus);
355 }
356 } else {
357 var start = aResult.indexOf("Auth=");
358 if (start == -1) {
359 // The Auth token could not be extracted
360 this.mLoggingIn = false;
361 this.invalidate();
362
363 // Retry login
364 this.loginAndContinue(aRequest.extraData);
365 } else {
366
367 this.mAuthToken = aResult.substring(start+5,
368 aResult.length - 1);
369 this.mLoggingIn = false;
370
371 if (this.savePassword) {
372 try {
373 passwordManagerSave(this.mGoogleUser,
374 this.mGooglePass);
375 } catch (e) {
376 // This error is non-fatal, but would constrict
377 // functionality
378 LOG("Error adding password to manager");
379 }
380 }
381
382 // Process Items that were requested while logging in
383 var request;
384 // Extra parentheses to avoid js strict warning.
385 while ((request = this.mItemQueue.shift())) {
386 LOG("Processing Queue Item: " + request.uri);
387 request.commit(this);
388 }
389 }
390 }
391 },
392
393 /**
394 * addItem
395 * Add a single item to google.
396 *
397 * @param aCalendar An instance of calIGoogleCalendar this
398 * request belongs to. The event will be
399 * added to this calendar.
400 * @param aItem An instance of calIEvent to add
401 * @param aResponseListener The function in aCalendar to call at
402 * completion.
403 * @param aExtraData Extra data to be passed to the response
404 * listener
405 */
cGS_addItem
406 addItem: function cGS_addItem(aCalendar,
407 aItem,
408 aResponseListener,
409 aExtraData) {
410
411 var request = new calGoogleRequest(this);
412 var xmlEntry = ItemToXMLEntry(aItem,
413 this.mGoogleUser,
414 this.googleFullName);
415
416 request.type = request.ADD;
417 request.uri = aCalendar.fullUri.spec;
418 request.setUploadData("application/atom+xml; charset=UTF-8", xmlEntry);
419 request.setResponseListener(aCalendar, aResponseListener);
420 request.extraData = aExtraData;
421 request.calendar = aCalendar;
422
423 this.asyncItemRequest(request);
424 },
425
426 /**
427 * modifyItem
428 * Modify a single item from google.
429 *
430 * @param aCalendar An instance of calIGoogleCalendar this
431 * request belongs to.
432 * @param aOldItem The instance of calIEvent before
433 * modification.
434 * @param aNewItem The instance of calIEvent after
435 * modification.
436 * @param aResponseListener The function in aCalendar to call at
437 * completion.
438 * @param aExtraData Extra data to be passed to the response
439 * listener
440 */
cGS_modifyItem
441 modifyItem: function cGS_modifyItem(aCalendar,
442 aOldItem,
443 aNewItem,
444 aResponseListener,
445 aExtraData) {
446
447 var request = new calGoogleRequest(this);
448
449 var xmlEntry = ItemToXMLEntry(aNewItem,
450 this.mGoogleUser,
451 this.googleFullName);
452
453 if (aOldItem.parentItem != aOldItem &&
454 !aOldItem.parentItem.recurrenceInfo.getExceptionFor(aOldItem.startDate, false)) {
455
456 // In this case we are modifying an occurence, not deleting it
457 request.type = request.ADD;
458 request.uri = aCalendar.fullUri.spec;
459 } else {
460 // We are making a negative exception or modifying a parent item
461 request.type = request.MODIFY;
462 request.uri = getItemEditURI(aOldItem);
463 }
464
465 request.setUploadData("application/atom+xml; charset=UTF-8", xmlEntry);
466 request.setResponseListener(aCalendar, aResponseListener);
467 request.extraData = aExtraData;
468 request.calendar = aCalendar;
469
470 this.asyncItemRequest(request);
471 },
472
473 /**
474 * deleteItem
475 * Delete a single item from google.
476 *
477 * @param aCalendar An instance of calIGoogleCalendar this
478 * request belongs to.
479 * belongs to.
480 * @param aItem An instance of calIEvent to delete
481 * @param aResponseListener The function in aCalendar to call at
482 * completion.
483 * @param aExtraData Extra data to be passed to the response
484 * listener
485 */
cGS_deleteItem
486 deleteItem: function cGS_deleteItem(aCalendar,
487 aItem,
488 aResponseListener,
489 aExtraData) {
490
491 var request = new calGoogleRequest(this);
492
493 request.type = request.DELETE;
494 request.uri = getItemEditURI(aItem);
495 request.setResponseListener(aCalendar, aResponseListener);
496 request.extraData = aExtraData;
497 request.calendar = aCalendar;
498
499 this.asyncItemRequest(request);
500 },
501
502 /**
503 * getItem
504 * Get a single item from google.
505 *
506 * @param aCalendar An instance of calIGoogleCalendar this
507 * request belongs to.
508 * @param aId The ID of the item requested
509 * @param aResponseListener The function in aCalendar to call at
510 * completion.
511 * @param aExtraData Extra data to be passed to the response
512 * listener
513 */
cGS_getItem
514 getItem: function cGS_getItem(aCalendar,
515 aId,
516 aResponseListener,
517 aExtraData) {
518
519
520 // XXX Due to google issue 399, there is no efficient way to get the
521 // by item id with all exceptions and an edit url. Therefore, just use
522 // getItems and add the id to the extradata.
523 //
524 return this.getItems(aCalendar,
525 null,
526 null,
527 null,
528 false,
529 aResponseListener,
530 aExtraData,
531 null);
532 },
533
534 /**
535 * getItems
536 * Get a Range of items from google.
537 *
538 * @param aCalendar An instance of calIGoogleCalendar this
539 * request belongs to.
540 * @param aCount The maximum number of items to return
541 * @param aRangeStart An instance of calIDateTime that limits
542 * the start date.
543 * @param aRangeEnd An instance of calIDateTime that limits
544 * the end date.
545 * @param aItemReturnOccurrences A boolean, wether to return single
546 * occurrences or not.
547 * @param aResponseListener The function in aCalendar to call at
548 * completion.
549 * @param aExtraData Extra data to be passed to the response
550 * listener
551 * @param aLastModified If specified, only events that have been
552 * modified since this date will be
553 * returned. This will also include
554 * deleted events. (optional)
555 */
556
cGS_getItems
557 getItems: function cGS_getItems(aCalendar,
558 aCount,
559 aRangeStart,
560 aRangeEnd,
561 aItemReturnOccurrences,
562 aResponseListener,
563 aExtraData,
564 aLastModified) {
565 // Requesting only a DATE returns items based on UTC. Therefore, we make
566 // sure both start and end dates include a time and timezone. This may
567 // not quite be what was requested, but I'd say its a shortcoming of
568 // rfc3339.
569 if (aRangeStart) {
570 aRangeStart = aRangeStart.clone();
571 aRangeStart.isDate = false;
572 }
573 if (aRangeEnd) {
574 aRangeEnd = aRangeEnd.clone();
575 aRangeEnd.isDate = false;
576 }
577
578 var rfcRangeStart = toRFC3339(aRangeStart);
579 var rfcRangeEnd = toRFC3339(aRangeEnd);
580
581 var request = new calGoogleRequest(this);
582
583 request.type = request.GET;
584 request.uri = aCalendar.fullUri.spec;
585 request.setResponseListener(aCalendar, aResponseListener);
586 request.extraData = aExtraData;
587 request.calendar = aCalendar;
588
589 // Request Parameters
590 request.addQueryParameter("max-results",
591 aCount ? aCount : kMANY_EVENTS);
592 request.addQueryParameter("singleevents", "false");
593 request.addQueryParameter("start-min", rfcRangeStart);
594 request.addQueryParameter("start-max", rfcRangeEnd);
595
596 if (aLastModified) {
597 var rfcLastModified = toRFC3339(aLastModified);
598 request.addQueryParameter("updated-min", rfcLastModified);
599 }
600
601 this.asyncItemRequest(request);
602 },
603
604 /**
605 * asyncItemRequest
606 * get or post an Item from or to Google using the Queue.
607 *
608 * @param aRequest The Request Object. This is an instance of
609 * calGoogleRequest
610 */
cGS_asyncItemRequest
611 asyncItemRequest: function cGS_asyncItemRequest(aRequest) {
612
613 if (!this.mLoggingIn && this.mAuthToken) {
614 // We are not currently logging in and we have an auth token, so
615 // directly try the login request
616 this.asyncRawRequest(aRequest);
617 } else {
618 // Push the request in the queue to be executed later
619 this.mItemQueue.push(aRequest);
620
621 LOG("Adding item " + aRequest.uri + " to queue");
622
623 // If we are logging in, then we are done since the passed request
624 // will be processed when the login is complete. Otherwise start
625 // logging in.
626 if (!this.mLoggingIn && this.mAuthToken == null) {
627 this.loginAndContinue(aRequest);
628 }
629 }
630 },
631
632 /**
633 * asyncRawRequest
634 * get or post an Item from or to Google without the Queue.
635 *
636 * @param aRequest The Request Object. This is an instance of
637 * calGoogleRequest
638 */
cGS_asyncRawRequest
639 asyncRawRequest: function cGS_asyncRawRequest(aRequest) {
640 // Request is handled by an instance of the calGoogleRequest
641 // We don't need to keep track of these requests, they
642 // pass to a listener or just die
643
644 ASSERT(aRequest);
645 aRequest.commit(this);
646 }
647 };