/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Google Calendar Provider code. * * The Initial Developer of the Original Code is * Philipp Kewisch (mozilla@kewis.ch) * Portions created by the Initial Developer are Copyright (C) 2006 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Joey Minta <jminta@gmail.com> * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** *//** * calGoogleCalendar * This Implements a calICalendar Object adapted to the Google Calendar * Provider. * * @class * @constructor */function calGoogleCalendar() { this.mObservers = new Array(); var calObject = this; function calAttrHelper(aAttr) { this.getAttr = function calAttrHelper_get() { // Note that you need to declare this in here, to avoid cyclic // getService calls. var calMgr = Cc["@mozilla.org/calendar/manager;1"] .getService(Ci.calICalendarManager); return calMgr.getCalendarPref(calObject, aAttr); }; this.setAttr = function calAttrHelper_set(aValue) { var calMgr = Cc["@mozilla.org/calendar/manager;1"] .getService(Ci.calICalendarManager); calMgr.setCalendarPref(calObject, aAttr, aValue); return aValue; }; } var prefAttrs = ["name", "supressAlarms"]; for each (var attr in prefAttrs) { var helper = new calAttrHelper(attr); this.__defineGetter__(attr, helper.getAttr); this.__defineSetter__(attr, helper.setAttr); }}calGoogleCalendar.prototype = { QueryInterface: function cGC_QueryInterface(aIID) { if (!aIID.equals(Ci.nsISupports) && !aIID.equals(Ci.calICalendar)) { throw Cr.NS_ERROR_NO_INTERFACE; } return this; }, /* Member Variables */ mSession: null, mObservers: null, mReadOnly: false, mUri: null, mFullUri: null, mCalendarName: null, /* * Google Calendar Provider attributes */ /** * readonly attribute googleCalendarName * Google's Calendar name. This represents the <calendar name> in * http://www.google.com/calendar/feeds/<calendar name>/private/full */ get googleCalendarName() { return this.mCalendarName; }, /** * attribute session * An calGoogleSession Object that handles the session requests. */ get session() { return this.mSession; }, set session(v) { return this.mSession = v; }, /** * notifyObservers * Notify this calendar's observers that a specific event has happened * * @param aEvent The Event that happened. * @param aArgs An array of arguments that is passed to the observer */ notifyObservers: function cGC_notifyObservers(aEvent, aArgs) { for each (var obs in this.mObservers) { try { obs[aEvent].apply(obs, aArgs); } catch (e) { Components.utils.reportError(e); } } }, /** * findSession * Populates the Session Object based on the preferences or the result of a * login prompt. * * @param aIgnoreExistingSession If set, find the session regardless of * whether the session has been previously set * or not */ findSession: function cGC_findSession(aIgnoreExistingSession) { if (this.mSession && !aIgnoreExistingSession) return; // We need to find out which Google account fits to this calendar. var googleUser = getCalendarPref(this, "googleUser"); if (googleUser) { this.mSession = getSessionByUsername(googleUser); } else { // We have no user, therefore we need to ask the user. Show a // user/password prompt and set the session based on those // values. var username = { value: this.mCalendarName }; var password = { value: null }; var savePassword = { value: false }; if (getCalendarCredentials(this.mCalendarName, username, password, savePassword)) { this.mSession = getSessionByUsername(username.value); this.mSession.googlePassword = password.value; this.mSession.savePassword = savePassword.value; setCalendarPref(this, "googleUser", "CHAR", this.mSession.googleUser); } } }, /* * implement calICalendar */ get readOnly() { return this.mReadOnly; }, set readOnly(v) { return this.mReadOnly = v; }, get type() { return "gdata"; }, get uri() { return this.mUri; }, get fullUri() { return this.mFullUri; }, set uri(aUri) { // Parse google url, catch private cookies, public calendars, // basic and full types, bogus ics file extensions, invalid hostnames var re = new RegExp("/calendar/(feeds|ical)/" + "([^/]+)/(public|private)-?([^/]+)?/" + "(full|basic)(.ics)?$"); var matches = aUri.path.match(re); if (!matches) { throw new Components.Exception(aUri, Cr.NS_ERROR_MALFORMED_URI); } // Set internal Calendar Name this.mCalendarName = decodeURIComponent(matches[2]); var ioService = Cc["@mozilla.org/network/io-service;1"] .getService(Ci.nsIIOService); // Set normalized url. We need the full xml stream and private // access. We need private visibility and full projection this.mFullUri = ioService.newURI("http://www.google.com" + "/calendar/feeds/" + matches[2] + "/private/full", null, null); // Remember the uri as it was passed, in case the calendar manager // relies on it. this.mUri = aUri; this.findSession(true); return this.mUri; }, get canRefresh() { return false; }, addObserver: function cGC_addObserver(aObserver) { if (this.mObservers.indexOf(aObserver) == -1) { this.mObservers.push(aObserver); } }, removeObserver: function cGC_removeObserver(aObserver) { function cGC_removeObserver_remove(obj) { return ( obj != aObserver ); } this.mObservers = this.mObservers.filter(cGC_removeObserver_remove); }, adoptItem: function cGC_adoptItem(aItem, aListener) { LOG("Adding item " + aItem.title); try { // Check if calendar is readonly if (this.mReadOnly) { throw new Components.Exception(Ci.calIErrors.CAL_IS_READONLY); } // Make sure the item is an event aItem = aItem.QueryInterface(Ci.calIEvent); // Check if we have a session. If not, then the user has canceled // the login prompt. if (!this.mSession) { this.findSession(); } this.mSession.addItem(this, aItem, this.addItem_response, aListener); } catch(e) { LOG("adoptItem failed before request " + aItem.title + "\n:" + e); if (aListener != null) { aListener.onOperationComplete(this, e.result, Ci.calIOperationListener.ADD, null, e.message); } } }, addItem: function cGC_addItem(aItem, aListener) { // Google assigns an ID to every event added. Any id set here or in // adoptItem will be overridden. return this.adoptItem( aItem.clone(), aListener ); }, modifyItem: function cGC_modifyItem(aNewItem, aOldItem, aListener) { LOG("Modifying item " + aOldItem.title); try { if (this.mReadOnly) { throw new Components.Exception(Ci.calIErrors.CAL_IS_READONLY); } // Check if we have a session. If not, then the user has canceled // the login prompt. if (!this.mSession) { this.findSession(); } // Check if the item is an occurrence. Until bug 362650 is solved, // there is no support for changing single ocurrences. if (aOldItem.getProperty("X-GOOGLE-ITEM-IS-OCCURRENCE")) { throw new Components.Exception(Cr.NS_ERROR_NOT_IMPLEMENTED); } // Check if enough fields have changed to warrant sending the event // to google. This saves network traffic. if (relevantFieldsMatch(aOldItem, aNewItem)) { LOG("Not requesting item modification for " + aOldItem.id + "(" + aOldItem.title + "), relevant fields match"); if (aListener != null) { aListener.onOperationComplete(this, Cr.NS_OK, Ci.calIOperationListener.MODIFY, aNewItem.id, aNewItem); } this.notifyObservers("onModifyItem", [aNewItem, aOldItem]); return; } // We need the old item in the response so the observer can be // called correctly. var extradata = { olditem: aOldItem, listener: aListener }; this.mSession.modifyItem(this, aNewItem, this.modifyItem_response, extradata); } catch(e) { LOG("modifyItem failed before request" + aNewItem.title + "(" + aNewItem.id + "):\n" + e); if (aListener != null) { aListener.onOperationComplete(this, e.result, Ci.calIOperationListener.MODIFY, null, e.message); } } }, deleteItem: function cGC_deleteItem(aItem, aListener) { LOG("Deleting item " + aItem.title + "(" + aItem.id + ")"); try { if (this.mReadOnly) { throw new Components.Exception(Ci.calIErrors.CAL_IS_READONLY); } // Check if we have a session. If not, then the user has canceled // the login prompt. if (!this.mSession) { this.findSession(); } // We need the item in the response, since google dosen't return any // item XML data on delete, and we need to call the observers. var extradata = { listener: aListener, item: aItem }; this.mSession.deleteItem(this, aItem, this.deleteItem_response, extradata); } catch(e) { LOG("deleteItem failed before request for" + aItem.title + "(" + aItem.id + "):\n" + e); if (aListener != null) { aListener.onOperationComplete(this, e.result, Ci.calIOperationListener.DELETE, null, e.message); } } }, getItem: function cGC_getItem(aId, aListener) { // This function needs a test case using mechanisms in bug 365212 LOG("Getting item with id " + aId); try { // Check if we have a session. If not, then the user has canceled // the login prompt. if (!this.mSession) { this.findSession(); } this.mSession.getItem(this, aId, this.getItem_response, aListener); } catch (e) { LOG("getItem failed before request " + aItem.title + "(" + aItem.id + "):\n" + e); if (aListener != null) { aListener.onOperationComplete(this, e.result, Ci.calIOperationListener.GET, null, e.message); } } }, getItems: function cGC_getItems(aItemFilter, aCount, aRangeStart, aRangeEnd, aListener) { try { // Check if we have a session. If not, then the user has canceled // the login prompt. if (!this.mSession) { this.findSession(); } // item base type var wantEvents = ((aItemFilter & Ci.calICalendar.ITEM_FILTER_TYPE_EVENT) != 0); var wantTodos = ((aItemFilter & Ci.calICalendar.ITEM_FILTER_TYPE_TODO) != 0); // check if events are wanted if (!wantEvents && !wantTodos) { // Nothing to do. The onOperationComplete in the catch block // below will catch this. throw new Components.Exception(Cr.NS_OK); } else if (wantTodos && !wantEvents) { throw new Components.Exception(Cr.NS_ERROR_NOT_IMPLEMENTED); } // return occurrences? var itemReturnOccurrences = ((aItemFilter & Ci.calICalendar.ITEM_FILTER_CLASS_OCCURRENCES) != 0); var extradata = { itemfilter: aItemFilter, listener: aListener }; this.mSession.getItems(this, aCount, aRangeStart, aRangeEnd, itemReturnOccurrences, this.getItems_response, extradata); } catch (e) { if (aListener != null) { aListener.onOperationComplete(this, e.result, Ci.calIOperationListener.GET, null, e.message); } } }, refresh: function cGC_refresh() { }, startBatch: function cGC_startBatch() { this.notifyObservers("onStartBatch", []); }, endBatch: function cGC_endBatch() { this.notifyObservers("onEndBatch", []); }, /* * Google Calendar Provider Response functions */ /** * addItem_response * Response callback, called by the session object when an item was added * * @param aRequest The request object that initiated the request * @param aStatus The response code. This is a Cr.* code * @param aResult In case of an error, this is the error string, otherwise * an XML representation of the added item. */ addItem_response: function cGC_addItem_response(aRequest, aStatus, aResult) { var item = this.general_response(Ci.calIOperationListener.ADD, aResult, aStatus, aRequest.extraData); // Notify Observers if (item) { this.notifyObservers("onAddItem", [item]); } }, /** * modifyItem_response * Response callback, called by the session object when an item was modified * * @param aRequest The request object that initiated the request * @param aStatus The response code. This is a Cr.* Code * @param aResult In case of an error, this is the error string, otherwise * an XML representation of the modified item */ modifyItem_response: function cGC_modifyItem_response(aRequest, aStatus, aResult) { var item = this.general_response(Ci.calIOperationListener.MODIFY, aResult, aStatus, aRequest.extraData.listener, aRequest); // Notify Observers if (item) { this.notifyObservers("onModifyItem", [item, aRequest.extraData.olditem]); } }, /** * deleteItem_response * Response callback, called by the session object when an Item was deleted * * @param aRequest The request object that initiated the request * @param aStatus The response code. This is a Cr.* Code * @param aResult In case of an error, this is the error string, otherwise * this may be empty. */ deleteItem_response: function cGC_deleteItem_response(aRequest, aStatus, aResult) { // The reason we are not using general_response here is because deleted // items are not returned as xml from google. We need to pass the item // we saved with the request. try { // Check if the call succeeded if (aStatus != Cr.NS_OK) { throw new Components.Exception(aResult, aStatus); } // All operations need to call onOperationComplete if (aRequest.extraData.listener) { LOG("Deleting item " + aRequest.extraData.item.id + " successful"); aRequest.extraData.listener.onOperationComplete(this, Cr.NS_OK, Ci.calIOperationListener.DELETE, aRequest.extraData.item.id, aRequest.extraData.item); } // Notify Observers this.notifyObservers("onDeleteItem", [aRequest.extraData.item]); } catch(e) { LOG("Deleting item " + aRequest.extraData.item.id + " failed"); // Operation failed if (aRequest.extraData.listener) { aRequest.extraData.listener.onOperationComplete(this, e.result, Ci.calIOperationListener.DELETE, null, e.message); } } }, /** * getItem_response * Response callback, called by the session object when a single Item was * downloaded. * * @param aRequest The request object that initiated the request * @param aStatus The response code. This is a Cr.* Code * @param aResult In case of an error, this is the error string, otherwise * an XML representation of the requested item */ getItem_response: function cGC_getItem_response(aRequest, aStatus, aResult) { // our general response does it all for us. I hope. this.general_response(Ci.calIOperationListener.GET, aResult, aStatus, aRequest.extraData.listener); }, /** * getItems_response * Response callback, called by the session object when an Item feed was * downloaded. * * @param aRequest The request object that initiated the request * @param aStatus The response code. This is a Cr.* Code * @param aResult In case of an error, this is the error string, otherwise * an XML feed with the requested items. */ getItems_response: function cGC_getItems_response(aRequest, aStatus, aResult) { LOG("Recieved response for " + aRequest.uri); try { // Check if the call succeeded if (aStatus != Cr.NS_OK) { throw new Components.Exception(aResult, aStatus); } // Prepare Namespaces var gCal = new Namespace("gCal", "http://schemas.google.com/gCal/2005"); var gd = new Namespace("gd", "http://schemas.google.com/g/2005"); var atom = new Namespace("", "http://www.w3.org/2005/Atom"); default xml namespace = atom; // A feed was passed back, parse it. Due to bug 336551 we need to // filter out the <?xml...?> part. var xml = new XML(aResult.substring(38)); var timezone = xml.gCal::timezone.@value.toString() || "UTC"; // PASTEBIN uncommenting this line makes it work // xml.link.(@rel); // We might be able to get the full name through this feed's author // tags. We need to make sure we have a session for that. if(!this.mSession) { this.findSession(); } if (xml.author.email == this.mSession.googleUser) { this.mSession.googleFullName = xml.author.name.toString(); } // Parse all <entry> tags for each (var entry in xml.entry) { var item = XMLEntryToItem(entry, timezone); if (item) { var itemReturnOccurrences = ((aRequest.extraData.itemfilter & Ci.calICalendar.ITEM_FILTER_CLASS_OCCURRENCES) != 0); var itemIsOccurrence = item.getProperty("X-GOOGLE-ITEM-IS-OCCURRENCE"); if ((itemReturnOccurrences && itemIsOccurrence) || !itemIsOccurrence) { LOG("Parsing entry:\n" + entry + "\n"); item.calendar = this; item.makeImmutable(); LOGitem(item); aRequest.extraData.listener.onGetResult(this, Cr.NS_OK, Ci.calIEvent, null, 1, [item]); } } else { LOG("Notice: An Item was skipped. Probably it has" + " features that are not supported, or it was" + " canceled"); } } // Operation Completed successfully. if (aRequest.extraData.listener != null) { aRequest.extraData.listener.onOperationComplete(this, Cr.NS_OK, Ci.calIOperationListener.GET, null, null); } } catch(e) { LOG("Error getting items:\n" + e); // Operation failed if (aRequest.extraData.listener != null) { aRequest.extraData.listener.onOperationComplete(this, e.result, Ci.calIOperationListener.GET, null, e.message); } } }, /** * general_response * Handles common actions for multiple response types. This does not notify * observers. * * @param aOperation The operation type (Ci.calIOperationListener.*) * @param aItemString The string represenation of the item * @param aStatus The response code. This is a Cr.* * error code * @param aListener The listener to be called on completion * (an instance of calIOperationListener) * * @return The Item as a calIEvent */ general_response: function cGC_general_response(aOperation, aItemString, aStatus, aListener) { try { // Check if the call succeeded, if not then aItemString is an error // message if (aStatus != Cr.NS_OK) { throw new Components.Exception(aItemString, aStatus); } // An Item was passed back, parse it. Due to bug 336551 we need to // filter out the <?xml...?> part. var xml = new XML(aItemString.substring(38)); // Get the local timezone from the preferences var timezone = getPrefSafe("calendar.timezone.local"); // Parse the Item with the given timezone var item = XMLEntryToItem(xml, timezone); if (!item) { // This can happen if the throw Cr.NS_ERROR_FAILURE; } LOGitem(item); item.calendar = this; item.makeImmutable(); // GET operations need to call onGetResult if (aOperation == Ci.calIOperationListener.GET) { aListener.onGetResult(this, Cr.NS_OK, Ci.calIEvent, null, 1, [item]); } // All operations need to call onOperationComplete if (aListener) { aListener.onOperationComplete(this, Cr.NS_OK, aOperation, (item ? item.id : null), item); } return item; } catch(e) { LOG("General response failed: " + e); // Operation failed aListener.onOperationComplete(this, e.result, aOperation, null, e.message); } }};