diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/icaljs/calIDateTimeJS.idl thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/icaljs/calIDateTimeJS.idl --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/icaljs/calIDateTimeJS.idl 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/icaljs/calIDateTimeJS.idl 2014-12-15 12:28:31.000000000 +0000 @@ -0,0 +1,223 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface calIDuration; +interface calITimezone; + +[scriptable, uuid(fe3e9a58-2938-4b2c-9085-4989d5f7244f)] +interface calIDateTime : nsISupports +{ + /** + * isMutable is true if this instance is modifiable. + * If isMutable is false, any attempts to modify + * the object will throw NS_ERROR_OBJECT_IS_IMMUTABLE. + */ + readonly attribute boolean isMutable; + + /** + * Make this calIDateTime instance immutable. + */ + void makeImmutable(); + + /** + * Clone this calIDateTime instance into a new + * mutable object. + */ + calIDateTime clone(); + + /** + * valid is true if this object contains a valid + * time/date. + */ + // true if this thing is set/valid + readonly attribute boolean isValid; + + /** + * nativeTime contains this instance's PRTime value relative + * to the UTC epoch, regardless of the timezone that's set + * on this instance. If nativeTime is set, the given UTC PRTime + * value is exploded into year/month/etc, forcing the timezone + * setting to UTC. + * + * @warning: When the timezone is set to 'floating', this will return + * the nativeTime as-if the timezone was UTC. Take this into account + * when comparing values. + * + * @note on objects that are pinned to a timezone and have isDate set, + * nativeTime will be 00:00:00 in the timezone of that date, not 00:00:00 in + * UTC. + */ + attribute PRTime nativeTime; + + /** + * Full 4-digit year value (e.g. "1989", "2004") + */ + attribute short year; + + /** + * Month, 0-11, 0 = January + */ + attribute short month; + + /** + * Day of month, 1-[28,29,30,31] + */ + attribute short day; + + /** + * Hour, 0-23 + */ + attribute short hour; + + /** + * Minute, 0-59 + */ + attribute short minute; + + /** + * Second, 0-59 + */ + attribute short second; + + /** + * Gets or sets the timezone of this calIDateTime instance. + * Setting the timezone does not change the actual date/time components; + * to convert between timezones, use getInTimezone(). + * + * @throws NS_ERROR_INVALID_ARG if null is passed in. + */ + attribute calITimezone timezone; + + /** + * Resets the datetime object. + * + * @param year full 4-digit year value (e.g. "1989", "2004") + * @param month month, 0-11, 0 = January + * @param day day of month, 1-[28,29,31] + * @param hour hour, 0-23 + * @param minute minute, 0-59 + * @param second decond, 0-59 + * @param timezone timezone + * + * The passed datetime will be normalized, e.g. a minute value of 60 will + * increase the hour. + * + * @throws NS_ERROR_INVALID_ARG if no timezone is passed in. + */ + void resetTo(in short year, + in short month, + in short day, + in short hour, + in short minute, + in short second, + in calITimezone timezone); + + /** + * The offset of the timezone this datetime is in, relative to UTC, in + * seconds. A positive number means that the timezone is ahead of UTC. + */ + readonly attribute long timezoneOffset; + + /** + * isDate indicates that this calIDateTime instance represents a date + * (a whole day), and not a specific time on that day. If isDate is set, + * accessing the hour/minute/second fields will return 0, and and setting + * them is an illegal operation. + */ + attribute boolean isDate; + + /* + * computed values + */ + + /** + * Day of the week. 0-6, with Sunday = 0. + */ + readonly attribute short weekday; + + /** + * Day of the year, 1-[365,366]. + */ + readonly attribute short yearday; + + /* + * Methods + */ + + /** + * Resets this instance to Jan 1, 1970 00:00:00 UTC. + */ + void reset(); + + /** + * Return a string representation of this instance. + */ + AUTF8String toString(); + + /** + * Return a new calIDateTime instance that's the result of + * converting this one into the given timezone. Valid values + * for aTimezone are the same as the timezone field. If + * the "floating" timezone is given, then this object + * is just cloned, and the timezone is set to floating. + */ + calIDateTime getInTimezone(in calITimezone aTimezone); + + // add the given calIDateTime, treating it as a duration, to + // this item. + // XXX will change + void addDuration (in calIDuration aDuration); + + // Subtract two dates and return a duration + // returns duration of this - aOtherDate + // if aOtherDate is > this the duration will be negative + calIDuration subtractDate (in calIDateTime aOtherDate); + + /** + * Compare this calIDateTime instance to aOther. Returns -1, 0, 1 to + * indicate if this < aOther, this == aOther, or this > aOther, + * respectively. + * + * This comparison is timezone-aware; the given values are converted + * to a common timezone before comparing. If either this or aOther is + * floating, both objects are treated as floating for the comparison. + * + * If either this or aOther has isDate set, then only the date portion is + * compared. + * + * @exception calIErrors.INVALID_TIMEZONE bad timezone on this object + * (not the argument object) + */ + long compare (in calIDateTime aOther); + + // + // Some helper getters for calculating useful ranges + // + + /** + * Returns SUNDAY of the given datetime object's week. + */ + readonly attribute calIDateTime startOfWeek; + + /** + * Returns SATURDAY of the datetime object's week. + */ + readonly attribute calIDateTime endOfWeek; + + // the start/end of the current object's month + readonly attribute calIDateTime startOfMonth; + readonly attribute calIDateTime endOfMonth; + + // the start/end of the current object's year + readonly attribute calIDateTime startOfYear; + readonly attribute calIDateTime endOfYear; + + /** + * This object as either an iCalendar DATE or DATETIME string, as + * appropriate and sets the timezone to either UTC or floating. + */ + attribute ACString icalString; +}; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/icaljs/calIDurationJS.idl thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/icaljs/calIDurationJS.idl --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/icaljs/calIDurationJS.idl 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/icaljs/calIDurationJS.idl 2014-12-15 12:28:31.000000000 +0000 @@ -0,0 +1,104 @@ +/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(78537f21-fd5c-4e02-ab26-8ff6a3d946cb)] +interface calIDuration : nsISupports +{ + /** + * isMutable is true if this instance is modifiable. + * If isMutable is false, any attempts to modify + * the object will throw CAL_ERROR_ITEM_IS_MUTABLE. + */ + readonly attribute boolean isMutable; + + /** + * Make this calIDuration instance immutable. + */ + void makeImmutable(); + + /** + * Clone this calIDuration instance into a new + * mutable object. + */ + calIDuration clone(); + + /** + * Is Negative + */ + attribute boolean isNegative; + + /** + * Weeks + */ + attribute short weeks; + + /** + * Days + */ + attribute short days; + + /** + * Hours + */ + attribute short hours; + + /** + * Minutes + */ + attribute short minutes; + + /** + * Seconds + */ + attribute short seconds; + + /** + * total duration in seconds + */ + attribute long inSeconds; + + /* + * Methods + */ + + /** + * Add a duration + */ + void addDuration(in calIDuration aDuration); + + /** + * Compare with another duration + * + * @param aOther to be compared with this object + * + * @return -1, 0, 1 if this < aOther, this == aOther, or this > aOther, + * respectively. + */ + long compare(in calIDuration aOther); + + /** + * Reset this duration to 0 + */ + void reset(); + + /** + * Normalize the duration + */ + void normalize(); + + /** + * Return a string representation of this instance. + */ + AUTF8String toString(); + + attribute jsval icalDuration; + + /** + * This object as an iCalendar DURATION string + */ + attribute ACString icalString; +}; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/icaljs/calIICSServiceJS.idl thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/icaljs/calIICSServiceJS.idl --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/icaljs/calIICSServiceJS.idl 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/icaljs/calIICSServiceJS.idl 2014-12-15 12:28:31.000000000 +0000 @@ -0,0 +1,260 @@ +/* -*- Mode: idl; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// XXX use strings for kind values instead of enumerated constants? + + +#include "nsISupports.idl" + +interface calIItemBase; +interface calIDateTime; +interface calIDuration; +interface calITimezone; +interface calITimezoneProvider; + +interface calIIcalProperty; +interface nsIUTF8StringEnumerator; +interface nsIInputStream; + +/** + * General notes: + * + * As with libical, use of getNextFoo(footype) is only valid if there have been + * no intervening getNextFoo(otherfootype)s, or removeFoo()s, or addFoo()s. In + * general, you want to do as little manipulation of your FooContainers as + * possible while iterating over them. + */ +[scriptable,uuid(59132cf2-e48c-4807-ab53-779f414a7fbc)] +interface calIIcalComponent : nsISupports +{ + /** + * The parent ical property + */ + readonly attribute calIIcalComponent parent; + + /** + * Access to the inner ical.js objects. Only use these if you know what you + * are doing. + */ + attribute jsval icalComponent; + attribute jsval icalTimezone; + + /** + * This is the value that an integer-valued getter will provide if + * there is no such property on the wrapped ical structure. + */ + const int32_t INVALID_VALUE = -1; + + /** + * @param kind ANY, XROOT, VCALENDAR, VEVENT, etc. + */ + calIIcalComponent getFirstSubcomponent(in AUTF8String componentType); + calIIcalComponent getNextSubcomponent(in AUTF8String componentType); + + readonly attribute AUTF8String componentType; + + attribute AUTF8String uid; + attribute AUTF8String prodid; + attribute AUTF8String version; + + /** + * PUBLISH, REQUEST, REPLY, etc. + */ + attribute AUTF8String method; + + /** + * TENTATIVE, CONFIRMED, CANCELLED, etc. + */ + attribute AUTF8String status; + + attribute AUTF8String summary; + attribute AUTF8String description; + attribute AUTF8String location; + attribute AUTF8String categories; + attribute AUTF8String URL; + + attribute int32_t priority; + + attribute calIDateTime startTime; + attribute calIDateTime endTime; + readonly attribute calIDuration duration; + attribute calIDateTime dueTime; + attribute calIDateTime stampTime; + + attribute calIDateTime createdTime; + attribute calIDateTime completedTime; + attribute calIDateTime lastModified; + + /** + * The recurrence ID, a.k.a. DTSTART-of-calculated-occurrence, + * or null if this isn't an occurrence. + */ + attribute calIDateTime recurrenceId; + + AUTF8String serializeToICS(); + + /** + * Return a string representation of this instance. + */ + AUTF8String toString(); + + /** + * Serializes this component (and subcomponents) directly to an + * input stream. Typically used for performance to avoid + * unnecessary conversions and XPConnect traversals. + * + * @result an input stream which can be read to get the serialized + * version of this component, encoded in UTF-8. Implements + * nsISeekableStream so that it can be used with + * nsIUploadChannel. + */ + nsIInputStream serializeToICSStream(); + + void addSubcomponent(in calIIcalComponent comp); +// If you add then remove a property/component, the referenced +// timezones won't get purged out. There's currently no client code. +// void removeSubcomponent(in calIIcalComponent comp); + + /** + * @param kind ANY, ATTENDEE, X-WHATEVER, etc. + */ + calIIcalProperty getFirstProperty(in AUTF8String kind); + calIIcalProperty getNextProperty(in AUTF8String kind); + void addProperty(in calIIcalProperty prop); +// If you add then remove a property/component, the referenced +// timezones won't get purged out. There's currently no client code. +// void removeProperty(in calIIcalProperty prop); + + /** + * Timezones need special handling, as they must be + * emitted as children of VCALENDAR, but can be referenced by + * any sub component. + * Adding a second timezone (of the same TZID) will remove the + * first one. + */ + void addTimezoneReference(in calITimezone aTimezone); + + /** + * Returns an array of VTIMEZONE components. + * These are the timezones that are in use by this + * component and its children. + */ + void getReferencedTimezones(out uint32_t aCount, + [array,size_is(aCount),retval] out calITimezone aTimezones); + + /** + * Clones the component. The cloned component is decoupled from any parent. + * @return cloned component + */ + calIIcalComponent clone(); +}; + +[scriptable,uuid(5b13a69c-53d3-44a0-9203-f89f7e5e1604)] +interface calIIcalProperty : nsISupports +{ + /** + * The whole property as an ical string. + * @exception Any libical error will be thrown as an calIError::ICS_ error. + */ + readonly attribute AUTF8String icalString; + + /** + * Access to the inner ical.js objects. Only use these if you know what you + * are doing. + */ + attribute jsval icalProperty; + + /** + * The parent component containing this property + */ + readonly attribute calIIcalComponent parent; + + /** + * Return a string representation of this instance. + */ + AUTF8String toString(); + + /** + * The value of the property as string. + * The exception for properties of TEXT or X- type, those will be unescaped + * when getting, and also expects an unescaped string when setting. + * Datetime, numeric and other non-text types are represented as ical string + */ + attribute AUTF8String value; + + /** + * The value of the property in (escaped) ical format. + */ + attribute AUTF8String valueAsIcalString; + + /** + * The value of the property as date/datetime value, keeping + * track of the used timezone referenced in the owning component. + */ + attribute calIDateTime valueAsDatetime; + + // XXX attribute AUTF8String stringValueWithParams; ? + readonly attribute AUTF8String propertyName; + + AUTF8String getParameter(in AUTF8String paramname); + void setParameter(in AUTF8String paramname, in AUTF8String paramval); + + AUTF8String getFirstParameterName(); + AUTF8String getNextParameterName(); + + void removeParameter(in AUTF8String paramname); + void clearXParameters(); +}; + +[scriptable,uuid(eda9565f-f9bb-4846-b134-1e0653b2e767)] +interface calIIcsComponentParsingListener : nsISupports +{ + /** + * Called when the parsing has completed. + * + * @param rc The result code of parsing + * @param rootComp The root ical component that was parsed + */ + void onParsingComplete(in nsresult rc, in calIIcalComponent rootComp); +}; + +[scriptable,uuid(31e7636b-5a64-4d15-bc60-67b67cd85176)] +interface calIICSService : nsISupports +{ + /** + * Parses an ICS string and uses the passed tzProvider instance to + * resolve timezones not contained withing the VCALENDAR. + * + * @param serialized an ICS string + * @param tzProvider timezone provider used to resolve TZIDs + * not contained within the VCALENDAR; + * if null is passed, parsing falls back to + * using the timezone service + */ + calIIcalComponent parseICS(in AUTF8String serialized, + in calITimezoneProvider tzProvider); + + /** + * Asynchronously parse an ICS string + * + * @param serialized an ICS string + * @param tzProvider timezone provider used to resolve TZIDs + * not contained within the VCALENDAR; + * if null is passed, parsing falls back to + * using the timezone service + * @param listener The listener that notifies the root component + */ + void parseICSAsync(in AUTF8String serialized, + in calITimezoneProvider tzProvider, + in calIIcsComponentParsingListener listener); + + calIIcalComponent createIcalComponent(in AUTF8String kind); + calIIcalProperty createIcalProperty(in AUTF8String kind); + calIIcalProperty createIcalPropertyFromString(in AUTF8String str); + /* I wish I could write this function atop libical! + boolean isLegalParameterValue(in AUTF8String paramKind, + in AUTF8String paramValue); + */ +}; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/icaljs/calIPeriodJS.idl thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/icaljs/calIPeriodJS.idl --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/icaljs/calIPeriodJS.idl 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/icaljs/calIPeriodJS.idl 2014-12-15 12:28:31.000000000 +0000 @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface calIDateTime; +interface calIDuration; + +[scriptable,uuid(ace2a74c-bd08-476f-be8b-6565abc50339)] +interface calIPeriod : nsISupports +{ + attribute jsval icalPeriod; + + /** + * isMutable is true if this instance is modifiable. + * If isMutable is false, any attempts to modify + * the object will throw NS_ERROR_OBJECT_IS_IMMUTABLE. + */ + readonly attribute boolean isMutable; + + /** + * Make this calIPeriod instance immutable. + */ + void makeImmutable(); + + /** + * Clone this calIPeriod instance into a new + * mutable object. + */ + calIPeriod clone(); + + /** + * The start datetime of this period + */ + attribute calIDateTime start; + + /** + * The end datetime of this period + */ + attribute calIDateTime end; + + /** + * The duration, equal to end-start + */ + readonly attribute calIDuration duration; + + + /** + * Return a string representation of this instance. + */ + AUTF8String toString(); + + /** + * This object as an iCalendar DURATION string + */ + attribute ACString icalString; +}; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/icaljs/icaljs.manifest thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/icaljs/icaljs.manifest --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/icaljs/icaljs.manifest 2014-12-08 19:30:39.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/icaljs/icaljs.manifest 2014-12-15 12:28:31.000000000 +0000 @@ -15,3 +15,5 @@ component {6702eb17-a968-4b43-b562-0d0c5f8e9eb5} calICALJSComponents.js contract @mozilla.org/calendar/timezone;1 {6702eb17-a968-4b43-b562-0d0c5f8e9eb5} + +interfaces caldatetime_icaljs.xpt diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/icaljs/moz.build thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/icaljs/moz.build --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/icaljs/moz.build 2014-12-08 19:30:39.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/icaljs/moz.build 2014-12-15 12:28:31.000000000 +0000 @@ -3,6 +3,15 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +XPIDL_SOURCES += [ + 'calIDateTimeJS.idl', + 'calIDurationJS.idl', + 'calIICSServiceJS.idl', + 'calIPeriodJS.idl', +] + +XPIDL_MODULE = 'caldatetime_icaljs' + EXTRA_COMPONENTS += [ 'calICALJSComponents.js', ] diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/libical/build/libical.manifest thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/libical/build/libical.manifest --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/libical/build/libical.manifest 2014-12-08 19:30:39.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/libical/build/libical.manifest 2014-12-15 12:28:31.000000000 +0000 @@ -1 +1,2 @@ #expand binary-component __SHARED_LIBRARY__ +interfaces caldatetime_libical.xpt diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/libical/calIDuration.idl thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/libical/calIDuration.idl --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/libical/calIDuration.idl 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/libical/calIDuration.idl 2014-12-15 12:28:31.000000000 +0000 @@ -0,0 +1,107 @@ +/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[ptr] native icaldurationtypeptr(struct icaldurationtype); + +[scriptable, uuid(f5e1c987-e722-4dec-bf91-93d4062b504a)] +interface calIDuration : nsISupports +{ + /** + * isMutable is true if this instance is modifiable. + * If isMutable is false, any attempts to modify + * the object will throw CAL_ERROR_ITEM_IS_MUTABLE. + */ + readonly attribute boolean isMutable; + + /** + * Make this calIDuration instance immutable. + */ + void makeImmutable(); + + /** + * Clone this calIDuration instance into a new + * mutable object. + */ + calIDuration clone(); + + /** + * Is Negative + */ + attribute boolean isNegative; + + /** + * Weeks + */ + attribute short weeks; + + /** + * Days + */ + attribute short days; + + /** + * Hours + */ + attribute short hours; + + /** + * Minutes + */ + attribute short minutes; + + /** + * Seconds + */ + attribute short seconds; + + /** + * total duration in seconds + */ + attribute long inSeconds; + + /* + * Methods + */ + + /** + * Add a duration + */ + void addDuration(in calIDuration aDuration); + + /** + * Compare with another duration + * + * @param aOther to be compared with this object + * + * @return -1, 0, 1 if this < aOther, this == aOther, or this > aOther, + * respectively. + */ + long compare(in calIDuration aOther); + + /** + * Reset this duration to 0 + */ + void reset(); + + /** + * Normalize the duration + */ + void normalize(); + + /** + * Return a string representation of this instance. + */ + AUTF8String toString(); + + [noscript,notxpcom] void toIcalDuration(in icaldurationtypeptr idt); + attribute jsval icalDuration; + + /** + * This object as an iCalendar DURATION string + */ + attribute ACString icalString; +}; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/libical/calIICSService.idl thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/libical/calIICSService.idl --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/libical/calIICSService.idl 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/libical/calIICSService.idl 2014-12-15 12:28:31.000000000 +0000 @@ -0,0 +1,270 @@ +/* -*- Mode: idl; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// XXX use strings for kind values instead of enumerated constants? + + +#include "nsISupports.idl" + +interface calIItemBase; +interface calIDateTime; +interface calIDuration; +interface calITimezone; +interface calITimezoneProvider; + +interface calIIcalProperty; +interface nsIUTF8StringEnumerator; +interface nsIInputStream; + +[ptr] native icalpropertyptr(struct icalproperty_impl); +[ptr] native icalcomponentptr(struct icalcomponent_impl); +[ptr] native icaltimezoneptr(struct _icaltimezone); + +/** + * General notes: + * + * As with libical, use of getNextFoo(footype) is only valid if there have been + * no intervening getNextFoo(otherfootype)s, or removeFoo()s, or addFoo()s. In + * general, you want to do as little manipulation of your FooContainers as + * possible while iterating over them. + */ +[scriptable,uuid(d2fc0264-191e-435e-8ef2-b2ab1fa81ca9)] +interface calIIcalComponent : nsISupports +{ + /** + * The parent ical property + */ + readonly attribute calIIcalComponent parent; + + /** + * Access to the inner ical.js objects. Only use these if you know what you + * are doing. + */ + attribute jsval icalComponent; + attribute jsval icalTimezone; + + /** + * This is the value that an integer-valued getter will provide if + * there is no such property on the wrapped ical structure. + */ + const int32_t INVALID_VALUE = -1; + + /** + * @param kind ANY, XROOT, VCALENDAR, VEVENT, etc. + */ + calIIcalComponent getFirstSubcomponent(in AUTF8String componentType); + calIIcalComponent getNextSubcomponent(in AUTF8String componentType); + + readonly attribute AUTF8String componentType; + + attribute AUTF8String uid; + attribute AUTF8String prodid; + attribute AUTF8String version; + + /** + * PUBLISH, REQUEST, REPLY, etc. + */ + attribute AUTF8String method; + + /** + * TENTATIVE, CONFIRMED, CANCELLED, etc. + */ + attribute AUTF8String status; + + attribute AUTF8String summary; + attribute AUTF8String description; + attribute AUTF8String location; + attribute AUTF8String categories; + attribute AUTF8String URL; + + attribute int32_t priority; + + attribute calIDateTime startTime; + attribute calIDateTime endTime; + readonly attribute calIDuration duration; + attribute calIDateTime dueTime; + attribute calIDateTime stampTime; + + attribute calIDateTime createdTime; + attribute calIDateTime completedTime; + attribute calIDateTime lastModified; + + /** + * The recurrence ID, a.k.a. DTSTART-of-calculated-occurrence, + * or null if this isn't an occurrence. + */ + attribute calIDateTime recurrenceId; + + AUTF8String serializeToICS(); + + /** + * Return a string representation of this instance. + */ + AUTF8String toString(); + + /** + * Serializes this component (and subcomponents) directly to an + * input stream. Typically used for performance to avoid + * unnecessary conversions and XPConnect traversals. + * + * @result an input stream which can be read to get the serialized + * version of this component, encoded in UTF-8. Implements + * nsISeekableStream so that it can be used with + * nsIUploadChannel. + */ + nsIInputStream serializeToICSStream(); + + void addSubcomponent(in calIIcalComponent comp); +// If you add then remove a property/component, the referenced +// timezones won't get purged out. There's currently no client code. +// void removeSubcomponent(in calIIcalComponent comp); + + /** + * @param kind ANY, ATTENDEE, X-WHATEVER, etc. + */ + calIIcalProperty getFirstProperty(in AUTF8String kind); + calIIcalProperty getNextProperty(in AUTF8String kind); + void addProperty(in calIIcalProperty prop); +// If you add then remove a property/component, the referenced +// timezones won't get purged out. There's currently no client code. +// void removeProperty(in calIIcalProperty prop); + + /** + * Timezones need special handling, as they must be + * emitted as children of VCALENDAR, but can be referenced by + * any sub component. + * Adding a second timezone (of the same TZID) will remove the + * first one. + */ + void addTimezoneReference(in calITimezone aTimezone); + + /** + * Returns an array of VTIMEZONE components. + * These are the timezones that are in use by this + * component and its children. + */ + void getReferencedTimezones(out uint32_t aCount, + [array,size_is(aCount),retval] out calITimezone aTimezones); + + /** + * Clones the component. The cloned component is decoupled from any parent. + * @return cloned component + */ + calIIcalComponent clone(); + + [noscript,notxpcom] icalcomponentptr getLibicalComponent(); + [noscript,notxpcom] icaltimezoneptr getLibicalTimezone(); +}; + +[scriptable,uuid(e0b9067f-0a53-4724-9c69-63599681877e)] +interface calIIcalProperty : nsISupports +{ + /** + * The whole property as an ical string. + * @exception Any libical error will be thrown as an calIError::ICS_ error. + */ + readonly attribute AUTF8String icalString; + + /** + * Access to the inner ical.js objects. Only use these if you know what you + * are doing. + */ + attribute jsval icalProperty; + + /** + * The parent component containing this property + */ + readonly attribute calIIcalComponent parent; + + /** + * Return a string representation of this instance. + */ + AUTF8String toString(); + + /** + * The value of the property as string. + * The exception for properties of TEXT or X- type, those will be unescaped + * when getting, and also expects an unescaped string when setting. + * Datetime, numeric and other non-text types are represented as ical string + */ + attribute AUTF8String value; + + /** + * The value of the property in (escaped) ical format. + */ + attribute AUTF8String valueAsIcalString; + + /** + * The value of the property as date/datetime value, keeping + * track of the used timezone referenced in the owning component. + */ + attribute calIDateTime valueAsDatetime; + + // XXX attribute AUTF8String stringValueWithParams; ? + readonly attribute AUTF8String propertyName; + + AUTF8String getParameter(in AUTF8String paramname); + void setParameter(in AUTF8String paramname, in AUTF8String paramval); + + AUTF8String getFirstParameterName(); + AUTF8String getNextParameterName(); + + void removeParameter(in AUTF8String paramname); + void clearXParameters(); + + [noscript,notxpcom] icalpropertyptr getLibicalProperty(); + [noscript,notxpcom] icalcomponentptr getLibicalComponent(); +}; + +[scriptable,uuid(131b419f-1373-4013-b470-5359aaff4049)] +interface calIIcsComponentParsingListener : nsISupports +{ + /** + * Called when the parsing has completed. + * + * @param rc The result code of parsing + * @param rootComp The root ical component that was parsed + */ + void onParsingComplete(in nsresult rc, in calIIcalComponent rootComp); +}; + +[scriptable,uuid(fea79ff8-07ba-46ae-ac92-7f9d8c49c9d2)] +interface calIICSService : nsISupports +{ + /** + * Parses an ICS string and uses the passed tzProvider instance to + * resolve timezones not contained withing the VCALENDAR. + * + * @param serialized an ICS string + * @param tzProvider timezone provider used to resolve TZIDs + * not contained within the VCALENDAR; + * if null is passed, parsing falls back to + * using the timezone service + */ + calIIcalComponent parseICS(in AUTF8String serialized, + in calITimezoneProvider tzProvider); + + /** + * Asynchronously parse an ICS string + * + * @param serialized an ICS string + * @param tzProvider timezone provider used to resolve TZIDs + * not contained within the VCALENDAR; + * if null is passed, parsing falls back to + * using the timezone service + * @param listener The listener that notifies the root component + */ + void parseICSAsync(in AUTF8String serialized, + in calITimezoneProvider tzProvider, + in calIIcsComponentParsingListener listener); + + calIIcalComponent createIcalComponent(in AUTF8String kind); + calIIcalProperty createIcalProperty(in AUTF8String kind); + calIIcalProperty createIcalPropertyFromString(in AUTF8String str); + /* I wish I could write this function atop libical! + boolean isLegalParameterValue(in AUTF8String paramKind, + in AUTF8String paramValue); + */ +}; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/libical/calIPeriod.idl thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/libical/calIPeriod.idl --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/libical/calIPeriod.idl 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/libical/calIPeriod.idl 2014-12-15 12:28:31.000000000 +0000 @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface calIDateTime; +interface calIDuration; + +[ptr] native icalperiodtypeptr(struct icalperiodtype); + +[scriptable,uuid(04ee525f-96db-4731-8d61-688e754df24f)] +interface calIPeriod : nsISupports +{ + attribute jsval icalPeriod; + + /** + * isMutable is true if this instance is modifiable. + * If isMutable is false, any attempts to modify + * the object will throw NS_ERROR_OBJECT_IS_IMMUTABLE. + */ + readonly attribute boolean isMutable; + + /** + * Make this calIPeriod instance immutable. + */ + void makeImmutable(); + + /** + * Clone this calIPeriod instance into a new + * mutable object. + */ + calIPeriod clone(); + + /** + * The start datetime of this period + */ + attribute calIDateTime start; + + /** + * The end datetime of this period + */ + attribute calIDateTime end; + + /** + * The duration, equal to end-start + */ + readonly attribute calIDuration duration; + + + /** + * Return a string representation of this instance. + */ + AUTF8String toString(); + + [noscript,notxpcom] void toIcalPeriod(in icalperiodtypeptr idt); + + /** + * This object as an iCalendar DURATION string + */ + attribute ACString icalString; +}; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/libical/moz.build thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/libical/moz.build --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/backend/libical/moz.build 2014-12-08 19:30:39.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/backend/libical/moz.build 2014-12-15 12:28:31.000000000 +0000 @@ -7,6 +7,15 @@ 'build' ] +XPIDL_SOURCES += [ + 'calIDateTime.idl', + 'calIDuration.idl', + 'calIICSService.idl', + 'calIPeriod.idl', +] + +XPIDL_MODULE = 'caldatetime_libical' + SOURCES += [ 'calDateTime.cpp', 'calDuration.cpp', diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/content/dialogs/calendar-event-dialog.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/content/dialogs/calendar-event-dialog.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/content/dialogs/calendar-event-dialog.js 2014-12-08 19:30:39.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/content/dialogs/calendar-event-dialog.js 2014-12-15 12:28:31.000000000 +0000 @@ -2518,7 +2518,7 @@ if (aItemRepeatCall && repeatDeck.selectedIndex == 1) { if (!rule.isByCount || !rule.isFinite) { setElementValue("repeat-until-datepicker", - !rule.isByCount ? rule.untilDate.getInTimezone(cal.floating()).jsDate + !rule.isByCount ? cal.dateTimeToJsDate(rule.untilDate.getInTimezone(cal.floating())) : "forever"); } else { // Try to recover the last occurrence in 10(?) years. @@ -3689,7 +3689,7 @@ // Restore the previous date. Since we are checking an until date, // a null value for gUntilDate means repeat "forever". setElementValue("repeat-until-datepicker", - gUntilDate ? gUntilDate.getInTimezone(cal.floating()).jsDate + gUntilDate ? cal.dateTimeToJsDate(gUntilDate.getInTimezone(cal.floating())) : "forever"); gWarning = true; let callback = function() { diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/public/calIDateTime.idl thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/public/calIDateTime.idl --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/public/calIDateTime.idl 2014-12-08 19:30:39.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/public/calIDateTime.idl 1970-01-01 00:00:00.000000000 +0000 @@ -1,226 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "nsISupports.idl" - -interface calIDuration; -interface calITimezone; -[ptr] native icaltimetypeptr(struct icaltimetype); - -[scriptable, uuid(04139dff-a6f0-446d-9aec-2062df887ef2)] -interface calIDateTime : nsISupports -{ - /** - * isMutable is true if this instance is modifiable. - * If isMutable is false, any attempts to modify - * the object will throw NS_ERROR_OBJECT_IS_IMMUTABLE. - */ - readonly attribute boolean isMutable; - - /** - * Make this calIDateTime instance immutable. - */ - void makeImmutable(); - - /** - * Clone this calIDateTime instance into a new - * mutable object. - */ - calIDateTime clone(); - - /** - * valid is true if this object contains a valid - * time/date. - */ - // true if this thing is set/valid - readonly attribute boolean isValid; - - /** - * nativeTime contains this instance's PRTime value relative - * to the UTC epoch, regardless of the timezone that's set - * on this instance. If nativeTime is set, the given UTC PRTime - * value is exploded into year/month/etc, forcing the timezone - * setting to UTC. - * - * @warning: When the timezone is set to 'floating', this will return - * the nativeTime as-if the timezone was UTC. Take this into account - * when comparing values. - * - * @note on objects that are pinned to a timezone and have isDate set, - * nativeTime will be 00:00:00 in the timezone of that date, not 00:00:00 in - * UTC. - */ - attribute PRTime nativeTime; - - /** - * Full 4-digit year value (e.g. "1989", "2004") - */ - attribute short year; - - /** - * Month, 0-11, 0 = January - */ - attribute short month; - - /** - * Day of month, 1-[28,29,30,31] - */ - attribute short day; - - /** - * Hour, 0-23 - */ - attribute short hour; - - /** - * Minute, 0-59 - */ - attribute short minute; - - /** - * Second, 0-59 - */ - attribute short second; - - /** - * Gets or sets the timezone of this calIDateTime instance. - * Setting the timezone does not change the actual date/time components; - * to convert between timezones, use getInTimezone(). - * - * @throws NS_ERROR_INVALID_ARG if null is passed in. - */ - attribute calITimezone timezone; - - /** - * Resets the datetime object. - * - * @param year full 4-digit year value (e.g. "1989", "2004") - * @param month month, 0-11, 0 = January - * @param day day of month, 1-[28,29,31] - * @param hour hour, 0-23 - * @param minute minute, 0-59 - * @param second decond, 0-59 - * @param timezone timezone - * - * The passed datetime will be normalized, e.g. a minute value of 60 will - * increase the hour. - * - * @throws NS_ERROR_INVALID_ARG if no timezone is passed in. - */ - void resetTo(in short year, - in short month, - in short day, - in short hour, - in short minute, - in short second, - in calITimezone timezone); - - /** - * The offset of the timezone this datetime is in, relative to UTC, in - * seconds. A positive number means that the timezone is ahead of UTC. - */ - readonly attribute long timezoneOffset; - - /** - * isDate indicates that this calIDateTime instance represents a date - * (a whole day), and not a specific time on that day. If isDate is set, - * accessing the hour/minute/second fields will return 0, and and setting - * them is an illegal operation. - */ - attribute boolean isDate; - - /* - * computed values - */ - - /** - * Day of the week. 0-6, with Sunday = 0. - */ - readonly attribute short weekday; - - /** - * Day of the year, 1-[365,366]. - */ - readonly attribute short yearday; - - /* - * Methods - */ - - /** - * Resets this instance to Jan 1, 1970 00:00:00 UTC. - */ - void reset(); - - /** - * Return a string representation of this instance. - */ - AUTF8String toString(); - - /** - * Return a new calIDateTime instance that's the result of - * converting this one into the given timezone. Valid values - * for aTimezone are the same as the timezone field. If - * the "floating" timezone is given, then this object - * is just cloned, and the timezone is set to floating. - */ - calIDateTime getInTimezone(in calITimezone aTimezone); - - // add the given calIDateTime, treating it as a duration, to - // this item. - // XXX will change - void addDuration (in calIDuration aDuration); - - // Subtract two dates and return a duration - // returns duration of this - aOtherDate - // if aOtherDate is > this the duration will be negative - calIDuration subtractDate (in calIDateTime aOtherDate); - - /** - * Compare this calIDateTime instance to aOther. Returns -1, 0, 1 to - * indicate if this < aOther, this == aOther, or this > aOther, - * respectively. - * - * This comparison is timezone-aware; the given values are converted - * to a common timezone before comparing. If either this or aOther is - * floating, both objects are treated as floating for the comparison. - * - * If either this or aOther has isDate set, then only the date portion is - * compared. - * - * @exception calIErrors.INVALID_TIMEZONE bad timezone on this object - * (not the argument object) - */ - long compare (in calIDateTime aOther); - - // - // Some helper getters for calculating useful ranges - // - - /** - * Returns SUNDAY of the given datetime object's week. - */ - readonly attribute calIDateTime startOfWeek; - - /** - * Returns SATURDAY of the datetime object's week. - */ - readonly attribute calIDateTime endOfWeek; - - // the start/end of the current object's month - readonly attribute calIDateTime startOfMonth; - readonly attribute calIDateTime endOfMonth; - - // the start/end of the current object's year - readonly attribute calIDateTime startOfYear; - readonly attribute calIDateTime endOfYear; - - [noscript,notxpcom] void toIcalTime(in icaltimetypeptr itt); - - /** - * This object as either an iCalendar DATE or DATETIME string, as - * appropriate and sets the timezone to either UTC or floating. - */ - attribute ACString icalString; -}; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/public/calIDuration.idl thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/public/calIDuration.idl --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/public/calIDuration.idl 2014-12-08 19:30:39.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/public/calIDuration.idl 1970-01-01 00:00:00.000000000 +0000 @@ -1,107 +0,0 @@ -/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "nsISupports.idl" - -[ptr] native icaldurationtypeptr(struct icaldurationtype); - -[scriptable, uuid(d1d2e05d-5472-482e-b0a4-a9e43925d530)] -interface calIDuration : nsISupports -{ - /** - * isMutable is true if this instance is modifiable. - * If isMutable is false, any attempts to modify - * the object will throw CAL_ERROR_ITEM_IS_MUTABLE. - */ - readonly attribute boolean isMutable; - - /** - * Make this calIDuration instance immutable. - */ - void makeImmutable(); - - /** - * Clone this calIDuration instance into a new - * mutable object. - */ - calIDuration clone(); - - /** - * Is Negative - */ - attribute boolean isNegative; - - /** - * Weeks - */ - attribute short weeks; - - /** - * Days - */ - attribute short days; - - /** - * Hours - */ - attribute short hours; - - /** - * Minutes - */ - attribute short minutes; - - /** - * Seconds - */ - attribute short seconds; - - /** - * total duration in seconds - */ - attribute long inSeconds; - - /* - * Methods - */ - - /** - * Add a duration - */ - void addDuration(in calIDuration aDuration); - - /** - * Compare with another duration - * - * @param aOther to be compared with this object - * - * @return -1, 0, 1 if this < aOther, this == aOther, or this > aOther, - * respectively. - */ - long compare(in calIDuration aOther); - - /** - * Reset this duration to 0 - */ - void reset(); - - /** - * Normalize the duration - */ - void normalize(); - - /** - * Return a string representation of this instance. - */ - AUTF8String toString(); - - [noscript,notxpcom] void toIcalDuration(in icaldurationtypeptr idt); - attribute jsval icalDuration; - - /** - * This object as an iCalendar DURATION string - */ - attribute ACString icalString; -}; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/public/calIICSService.idl thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/public/calIICSService.idl --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/public/calIICSService.idl 2014-12-08 19:30:39.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/public/calIICSService.idl 1970-01-01 00:00:00.000000000 +0000 @@ -1,270 +0,0 @@ -/* -*- Mode: idl; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// XXX use strings for kind values instead of enumerated constants? - - -#include "nsISupports.idl" - -interface calIItemBase; -interface calIDateTime; -interface calIDuration; -interface calITimezone; -interface calITimezoneProvider; - -interface calIIcalProperty; -interface nsIUTF8StringEnumerator; -interface nsIInputStream; - -[ptr] native icalpropertyptr(struct icalproperty_impl); -[ptr] native icalcomponentptr(struct icalcomponent_impl); -[ptr] native icaltimezoneptr(struct _icaltimezone); - -/** - * General notes: - * - * As with libical, use of getNextFoo(footype) is only valid if there have been - * no intervening getNextFoo(otherfootype)s, or removeFoo()s, or addFoo()s. In - * general, you want to do as little manipulation of your FooContainers as - * possible while iterating over them. - */ -[scriptable,uuid(c4637c40-3c4c-4ecd-b802-8b5b46bdf5a4)] -interface calIIcalComponent : nsISupports -{ - /** - * The parent ical property - */ - readonly attribute calIIcalComponent parent; - - /** - * Access to the inner ical.js objects. Only use these if you know what you - * are doing. - */ - attribute jsval icalComponent; - attribute jsval icalTimezone; - - /** - * This is the value that an integer-valued getter will provide if - * there is no such property on the wrapped ical structure. - */ - const int32_t INVALID_VALUE = -1; - - /** - * @param kind ANY, XROOT, VCALENDAR, VEVENT, etc. - */ - calIIcalComponent getFirstSubcomponent(in AUTF8String componentType); - calIIcalComponent getNextSubcomponent(in AUTF8String componentType); - - readonly attribute AUTF8String componentType; - - attribute AUTF8String uid; - attribute AUTF8String prodid; - attribute AUTF8String version; - - /** - * PUBLISH, REQUEST, REPLY, etc. - */ - attribute AUTF8String method; - - /** - * TENTATIVE, CONFIRMED, CANCELLED, etc. - */ - attribute AUTF8String status; - - attribute AUTF8String summary; - attribute AUTF8String description; - attribute AUTF8String location; - attribute AUTF8String categories; - attribute AUTF8String URL; - - attribute int32_t priority; - - attribute calIDateTime startTime; - attribute calIDateTime endTime; - readonly attribute calIDuration duration; - attribute calIDateTime dueTime; - attribute calIDateTime stampTime; - - attribute calIDateTime createdTime; - attribute calIDateTime completedTime; - attribute calIDateTime lastModified; - - /** - * The recurrence ID, a.k.a. DTSTART-of-calculated-occurrence, - * or null if this isn't an occurrence. - */ - attribute calIDateTime recurrenceId; - - AUTF8String serializeToICS(); - - /** - * Return a string representation of this instance. - */ - AUTF8String toString(); - - /** - * Serializes this component (and subcomponents) directly to an - * input stream. Typically used for performance to avoid - * unnecessary conversions and XPConnect traversals. - * - * @result an input stream which can be read to get the serialized - * version of this component, encoded in UTF-8. Implements - * nsISeekableStream so that it can be used with - * nsIUploadChannel. - */ - nsIInputStream serializeToICSStream(); - - void addSubcomponent(in calIIcalComponent comp); -// If you add then remove a property/component, the referenced -// timezones won't get purged out. There's currently no client code. -// void removeSubcomponent(in calIIcalComponent comp); - - /** - * @param kind ANY, ATTENDEE, X-WHATEVER, etc. - */ - calIIcalProperty getFirstProperty(in AUTF8String kind); - calIIcalProperty getNextProperty(in AUTF8String kind); - void addProperty(in calIIcalProperty prop); -// If you add then remove a property/component, the referenced -// timezones won't get purged out. There's currently no client code. -// void removeProperty(in calIIcalProperty prop); - - /** - * Timezones need special handling, as they must be - * emitted as children of VCALENDAR, but can be referenced by - * any sub component. - * Adding a second timezone (of the same TZID) will remove the - * first one. - */ - void addTimezoneReference(in calITimezone aTimezone); - - /** - * Returns an array of VTIMEZONE components. - * These are the timezones that are in use by this - * component and its children. - */ - void getReferencedTimezones(out uint32_t aCount, - [array,size_is(aCount),retval] out calITimezone aTimezones); - - /** - * Clones the component. The cloned component is decoupled from any parent. - * @return cloned component - */ - calIIcalComponent clone(); - - [noscript,notxpcom] icalcomponentptr getLibicalComponent(); - [noscript,notxpcom] icaltimezoneptr getLibicalTimezone(); -}; - -[scriptable,uuid(17349a10-5d80-47fa-9bea-f22957357675)] -interface calIIcalProperty : nsISupports -{ - /** - * The whole property as an ical string. - * @exception Any libical error will be thrown as an calIError::ICS_ error. - */ - readonly attribute AUTF8String icalString; - - /** - * Access to the inner ical.js objects. Only use these if you know what you - * are doing. - */ - attribute jsval icalProperty; - - /** - * The parent component containing this property - */ - readonly attribute calIIcalComponent parent; - - /** - * Return a string representation of this instance. - */ - AUTF8String toString(); - - /** - * The value of the property as string. - * The exception for properties of TEXT or X- type, those will be unescaped - * when getting, and also expects an unescaped string when setting. - * Datetime, numeric and other non-text types are represented as ical string - */ - attribute AUTF8String value; - - /** - * The value of the property in (escaped) ical format. - */ - attribute AUTF8String valueAsIcalString; - - /** - * The value of the property as date/datetime value, keeping - * track of the used timezone referenced in the owning component. - */ - attribute calIDateTime valueAsDatetime; - - // XXX attribute AUTF8String stringValueWithParams; ? - readonly attribute AUTF8String propertyName; - - AUTF8String getParameter(in AUTF8String paramname); - void setParameter(in AUTF8String paramname, in AUTF8String paramval); - - AUTF8String getFirstParameterName(); - AUTF8String getNextParameterName(); - - void removeParameter(in AUTF8String paramname); - void clearXParameters(); - - [noscript,notxpcom] icalpropertyptr getLibicalProperty(); - [noscript,notxpcom] icalcomponentptr getLibicalComponent(); -}; - -[scriptable,uuid(41fb4cbd-9977-41c7-b179-ab567e4b9395)] -interface calIIcsComponentParsingListener : nsISupports -{ - /** - * Called when the parsing has completed. - * - * @param rc The result code of parsing - * @param rootComp The root ical component that was parsed - */ - void onParsingComplete(in nsresult rc, in calIIcalComponent rootComp); -}; - -[scriptable,uuid(ff407e5b-9780-4644-b594-b49f6612a9bf)] -interface calIICSService : nsISupports -{ - /** - * Parses an ICS string and uses the passed tzProvider instance to - * resolve timezones not contained withing the VCALENDAR. - * - * @param serialized an ICS string - * @param tzProvider timezone provider used to resolve TZIDs - * not contained within the VCALENDAR; - * if null is passed, parsing falls back to - * using the timezone service - */ - calIIcalComponent parseICS(in AUTF8String serialized, - in calITimezoneProvider tzProvider); - - /** - * Asynchronously parse an ICS string - * - * @param serialized an ICS string - * @param tzProvider timezone provider used to resolve TZIDs - * not contained within the VCALENDAR; - * if null is passed, parsing falls back to - * using the timezone service - * @param listener The listener that notifies the root component - */ - void parseICSAsync(in AUTF8String serialized, - in calITimezoneProvider tzProvider, - in calIIcsComponentParsingListener listener); - - calIIcalComponent createIcalComponent(in AUTF8String kind); - calIIcalProperty createIcalProperty(in AUTF8String kind); - calIIcalProperty createIcalPropertyFromString(in AUTF8String str); - /* I wish I could write this function atop libical! - boolean isLegalParameterValue(in AUTF8String paramKind, - in AUTF8String paramValue); - */ -}; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/public/calIPeriod.idl thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/public/calIPeriod.idl --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/public/calIPeriod.idl 2014-12-08 19:30:39.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/public/calIPeriod.idl 1970-01-01 00:00:00.000000000 +0000 @@ -1,62 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "nsISupports.idl" - -interface calIDateTime; -interface calIDuration; - -[ptr] native icalperiodtypeptr(struct icalperiodtype); - -[scriptable,uuid(03cde9c7-c864-4109-8bea-ebbff01cbab9)] -interface calIPeriod : nsISupports -{ - attribute jsval icalPeriod; - - /** - * isMutable is true if this instance is modifiable. - * If isMutable is false, any attempts to modify - * the object will throw NS_ERROR_OBJECT_IS_IMMUTABLE. - */ - readonly attribute boolean isMutable; - - /** - * Make this calIPeriod instance immutable. - */ - void makeImmutable(); - - /** - * Clone this calIPeriod instance into a new - * mutable object. - */ - calIPeriod clone(); - - /** - * The start datetime of this period - */ - attribute calIDateTime start; - - /** - * The end datetime of this period - */ - attribute calIDateTime end; - - /** - * The duration, equal to end-start - */ - readonly attribute calIDuration duration; - - - /** - * Return a string representation of this instance. - */ - AUTF8String toString(); - - [noscript,notxpcom] void toIcalPeriod(in icalperiodtypeptr idt); - - /** - * This object as an iCalendar DURATION string - */ - attribute ACString icalString; -}; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/public/moz.build thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/public/moz.build --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/base/public/moz.build 2014-12-08 19:30:39.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/base/public/moz.build 2014-12-15 12:28:31.000000000 +0000 @@ -16,22 +16,18 @@ 'calICalendarView.idl', 'calICalendarViewController.idl', 'calIChangeLog.idl', - 'calIDateTime.idl', 'calIDateTimeFormatter.idl', 'calIDeletedItems.idl', - 'calIDuration.idl', 'calIErrors.idl', 'calIEvent.idl', 'calIFreeBusyProvider.idl', 'calIIcsParser.idl', 'calIIcsSerializer.idl', - 'calIICSService.idl', 'calIImportExport.idl', 'calIItemBase.idl', 'calIItipItem.idl', 'calIItipTransport.idl', 'calIOperation.idl', - 'calIPeriod.idl', 'calIPrintFormatter.idl', 'calIRecurrenceDate.idl', 'calIRecurrenceInfo.idl', diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/locales/en-US/chrome/calendar/providers/gdata/gdata.properties thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/locales/en-US/chrome/calendar/providers/gdata/gdata.properties --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/locales/en-US/chrome/calendar/providers/gdata/gdata.properties 2014-12-08 19:30:40.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/locales/en-US/chrome/calendar/providers/gdata/gdata.properties 2014-12-15 12:28:32.000000000 +0000 @@ -23,3 +23,7 @@ providerOutdated=This version of the provider has expired, please update to the latest version. reminderOutOfRange=Google Calendar only allows reminders up to 4 weeks before the event starts. + +syncProgressEvent=Synchronizing %1$S event %2$S of %3$S +syncProgressTask=Synchronizing %1$S task %2$S of %3$S +syncStatus=Synchronizing Calendar %1$S diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleCalendar.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleCalendar.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleCalendar.js 2014-12-08 19:30:40.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleCalendar.js 2014-12-15 12:28:32.000000000 +0000 @@ -2,13 +2,27 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -Components.utils.import("resource://calendar/modules/calProviderUtils.jsm"); -Components.utils.import("resource://calendar/modules/calXMLUtils.jsm"); -Components.utils.import("resource://calendar/modules/calUtils.jsm"); -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); -Components.utils.import("resource://gre/modules/Preferences.jsm"); +Components.utils.import("resource://gdata-provider/modules/shim/Loader.jsm").shimIt(this); +Components.utils.import("resource://gdata-provider/modules/shim/Calendar.jsm"); + +CuImport("resource://gre/modules/Preferences.jsm", this); +CuImport("resource://gre/modules/Promise.jsm", this); +CuImport("resource://gre/modules/Services.jsm", this); +CuImport("resource://gre/modules/Task.jsm", this); +CuImport("resource://gre/modules/XPCOMUtils.jsm", this); + +CuImport("resource://calendar/modules/calProviderUtils.jsm", this); +CuImport("resource://calendar/modules/calUtils.jsm", this); + +CuImport("resource://gdata-provider/modules/gdataLogging.jsm", this); +CuImport("resource://gdata-provider/modules/gdataRequest.jsm", this); +CuImport("resource://gdata-provider/modules/gdataSession.jsm", this); +CuImport("resource://gdata-provider/modules/gdataUtils.jsm", this); const cICL = Components.interfaces.calIChangeLog; +const cIOL = Components.interfaces.calIOperationListener; + +const MIN_REFRESH_INTERVAL = 30; /** * calGoogleCalendar @@ -20,223 +34,242 @@ */ function calGoogleCalendar() { this.initProviderBase(); + this.mThrottle = Object.create(null); } + const calGoogleCalendarClassID = Components.ID("{d1a6e988-4b4d-45a5-ba46-43e501ea96e3}"); const calGoogleCalendarInterfaces = [ Components.interfaces.calICalendar, - Components.interfaces.calIGoogleCalendar, Components.interfaces.calISchedulingSupport, Components.interfaces.calIChangeLog ]; calGoogleCalendar.prototype = { __proto__: cal.ProviderBase.prototype, + classID: calGoogleCalendarClassID, QueryInterface: XPCOMUtils.generateQI(calGoogleCalendarInterfaces), classInfo: XPCOMUtils.generateCI({ - classID: calGoogleCalendarClassID, - contractID: "@mozilla.org/calendar/calendar;1?type=gdata", classDescription: "Google Calendar Provider", + contractID: "@mozilla.org/calendar/calendar;1?type=gdata", + classID: calGoogleCalendarClassID, interfaces: calGoogleCalendarInterfaces }), + /* Used to reset the local cache between releases */ + CACHE_DB_VERSION: 2, + /* Member Variables */ - mSession: null, - mFullUri: null, mCalendarName: null, - - /* - * Google Calendar Provider attributes - */ - - /** - * readonly attribute googleCalendarName - * Google's Calendar name. This represents the in - * http[s]://www.google.com/calendar/feeds//private/full - */ - get googleCalendarName() { - return this.mCalendarName; + mThrottle: null, + mThrottleLimits: { + "calendarList": 3600 * 1000, + "events": 30 * 1000, + "tasks": 30 * 1000 }, - get isDefaultCalendar() { - return !/@group\.calendar\.google\.com$/.test(this.mCalendarName); - }, + /* Public Members */ + session: null, /** - * attribute session - * An calGoogleSession Object that handles the session requests. + * Make sure a session is available. */ - get session() { - return this.mSession; - }, - set session(v) { - return this.mSession = v; - }, + ensureSession: function() { + if (!this.session) { + // Now actually set up the session + let sessionMgr = getGoogleSessionManager(); + this.session = sessionMgr.getSessionByCalendar(this, true); - get title() { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - set title(v) { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - - get access() { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - set access(v) { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + // Aside from setting up the session, bump the refresh interval to + // a higher value if its below the minimal refresh interval to + // avoid exceeding quota. + let interval = this.getProperty("refreshInterval"); + if (interval < MIN_REFRESH_INTERVAL && interval != 0) { + cal.LOG("[calGoogleCalendar] Sorry, auto-refresh intervals under " + MIN_REFRESH_INTERVAL + " minutes would cause the quota to be reached too fast."); + this.setProperty("refreshInterval", 2 * MIN_REFRESH_INTERVAL); + } + } }, - get selected() { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - set selected(v) { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + ensureWritable: function() { + // Check if calendar is readonly + if (this.readOnly) { + const cIE = Components.interfaces.calIErrors; + throw new Components.Exception("", cIE.CAL_IS_READONLY); + } }, - get hidden() { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - set hidden(v) { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, + get isDefaultCalendar() !this.mCalendarName.endsWith("@group.calendar.google.com"), - get color() { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - set color(v) { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - - get timezone() { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - set timezone(v) { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - /** - * 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 + /* + * implement calICalendar */ - findSession: function cGC_findSession(aIgnoreExistingSession) { - if (this.mSession && !aIgnoreExistingSession) { - return true; + get type() "gdata", + get providerID() "{a62ef8ec-5fdc-40c2-873c-223b8a6925cc}", + get canRefresh() true, + + get id() this.mID, + set id(val) { + let setter = this.__proto__.__proto__.__lookupSetter__("id"); + val = setter.call(this, val); + + if (this.id && this.uri) { + this.ensureSession(); } + return val; + }, - // We need to find out which Google account fits to this calendar. - let sessionMgr = getGoogleSessionManager(); - let googleUser = getCalendarPref(this, "googleUser"); - if (googleUser) { - this.mSession = sessionMgr.getSessionByUsername(googleUser, true); - } 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. - - let username = { value: null }; - if (this.isDefaultCalendar) { - // Only pre-fill the username if this is the default calendar, - // otherwise users might think the cryptic hash is the username - // they have to use. - username.value = this.mCalendarName; - } - let password = { value: null }; - let persist = { value: false }; - - if (getCalendarCredentials(this.mCalendarName, - username, - password, - persist)) { - this.mSession = sessionMgr.getSessionByUsername(username.value, - true); - - this.mSession.password = password.value; - this.mSession.persist = persist.value; - setCalendarPref(this, - "googleUser", - "CHAR", - this.mSession.userName); + get uri() this.mUri, + set uri(aUri) { + const protocols = ["http", "https", "webcal", "webcals"]; + this.mUri = aUri; + if (aUri && aUri.schemeIs("googleapi")) { + // new format: googleapi://session-id/?calendar=calhash@group.calendar.google.com&tasks=taskhash + let [fullUser, path] = aUri.path.substr(2).split("/", 2); + let parameters = new Map(path.substr(1).split("&").filter(Boolean) + .map(function(x) x.split("=", 2).map(decodeURIComponent))); + + if (parameters.size == 0) { + this.mCalendarName = fullUser; + this.mTasklistName = this.isDefaultCalendar ? "@default" : null; } else { - // The password dialog was canceled, disable the calendar. - this.setProperty("disabled", true); - return false; + this.mCalendarName = parameters.get("calendar"); + this.mTasklistName = parameters.get("tasks"); } - } - return true; - }, - /** - * ensureSession - * Make sure a session is available. If not, throw an exception - */ - ensureSession: function cGC_ensureSession() { - if (!this.mSession || - !this.mSession.password || - this.mSession.password == "") { - if (!this.findSession) { - throw new Components.Exception("Session was canceled", - Components.results.NS_ERROR_FAILURE); + // Users that installed 1.0 had an issue where secondary calendars + // were migrated to their own session. This code fixes that and + // should be removed once 1.0.1 has been out for a while. + let googleUser = Preferences.get("calendar.google.calPrefs." + fullUser + ".googleUser"); + if (googleUser && googleUser != fullUser) { + let newUri = "googleapi://" + googleUser + "/" + path; + cal.LOG("[calGoogleCalendar] Migrating url format from " + aUri.spec + " to " + newUri); + this.setProperty("uri", newUri); + this.mUri = Services.io.newURI(newUri, null, null); + } + + // Unit tests will use a local uri, if the magic parameter is passed. + let port = parameters.get("testport"); + if (port) { + cal.LOG("[calGoogleCalendar] Redirecting request to test port " + port); + API_BASE.EVENTS = "http://localhost:" + port + "/calendar/v3/"; + API_BASE.TASKS = "http://localhost:" + port + "/tasks/v1/"; + } + } else if (aUri && protocols.some(function(scheme) { return aUri.schemeIs(scheme); })) { + // Parse google url, catch private cookies, public calendars, + // basic and full types, bogus ics file extensions, invalid hostnames + let re = new RegExp("/calendar/(feeds|ical)/" + + "([^/]+)/(public|private|free-busy)-?([^/]+)?/" + + "(full|basic)(.ics)?$"); + + let matches = aUri.path.match(re); + if (matches) { + this.mCalendarName = decodeURIComponent(matches[2]); + + let googleUser = Preferences.get("calendar.google.calPrefs." + this.mCalendarName + ".googleUser"); + let newUri = "googleapi://" + (googleUser || this.mCalendarName) + "/?calendar=" + matches[2]; + + // Use the default task list, but only if this is the primary account. + if (googleUser && googleUser == this.mCalendarName) { + this.mTasklistName = "@default"; + newUri += "&tasks=%40default"; + } + + cal.LOG("[calGoogleCalendar] Migrating url format from " + aUri.spec + + " to " + newUri); + this.setProperty("uri", newUri); + this.mUri = Services.io.newURI(newUri, null, null); } } - }, - /* - * implement calICalendar - */ - get type() { - return "gdata"; + if (this.id && this.uri) { + this.ensureSession(); + } + + return this.mUri; }, - get providerID() { - return "{a62ef8ec-5fdc-40c2-873c-223b8a6925cc}"; + createEventsURI: function (/* ...extraParts */) { + let extraParts = Array.slice(arguments); + let eventsURI = null; + if (this.mCalendarName) { + let encodedName = encodeURIComponent(this.mCalendarName); + let parts = ["calendars", encodedName].concat(Array.filter(extraParts, Boolean)); + eventsURI = API_BASE.EVENTS + parts.join("/"); + } + return eventsURI; }, - get uri() { - return this.mUri; + createUsersURI: function(/* ...extraParts */) { + let extraParts = Array.slice(arguments); + let parts = ["users", "me"].concat(extraParts).map(encodeURIComponent); + return API_BASE.EVENTS + parts.join("/"); }, - get fullUri() { - return this.mFullUri; + createTasksURI: function(/* ...extraParts */) { + let extraParts = Array.slice(arguments); + let tasksURI = null; + if (this.mTasklistName) { + let encodedName = encodeURIComponent(this.mTasklistName); + let parts = ["lists", encodedName].concat(Array.filter(extraParts, Boolean)); + tasksURI = API_BASE.TASKS + parts.join("/"); + } + return tasksURI; }, - set uri(aUri) { - // Parse google url, catch private cookies, public calendars, - // basic and full types, bogus ics file extensions, invalid hostnames - let re = new RegExp("/calendar/(feeds|ical)/" + - "([^/]+)/(public|private)-?([^/]+)?/" + - "(full|basic)(.ics)?$"); - let matches = aUri.path.match(re); + STALE_TIME: 7 * 86400, - if (!matches) { - throw new Components.Exception(aUri, Components.results.NS_ERROR_MALFORMED_URI); + getUpdatedMin: function getUpdatedMin(aWhich) { + let updatedMin = null; + let lastUpdated = this.getProperty("lastUpdated." + aWhich); + if (lastUpdated) { + updatedMin = cal.createDateTime(lastUpdated); + let lastWeek = cal.now(); + lastWeek.second -= this.STALE_TIME; + if (updatedMin.compare(lastWeek) <= 0) { + cal.LOG("[calGoogleCalendar] Last updated time for " + aWhich + + " is very old, doing full sync"); + this.resetLog(); + updatedMin = null; + } } + return updatedMin ? getCorrectedDate(updatedMin) : null; + }, - // Set internal Calendar Name - this.mCalendarName = decodeURIComponent(matches[2]); + checkThrottle: function(type) { + let shouldRequest = true; + let now = new Date().getTime(); - // Set normalized url. We need private visibility and full projection - this.mFullUri = aUri.clone(); - this.mFullUri.path = "/calendar/feeds/" + matches[2] + "/private/full"; + if (type in this.mThrottle) { + let then = this.mThrottle[type]; - // Remember the uri as it was passed, in case the calendar manager - // relies on it. - this.mUri = aUri; + if (now - then < this.mThrottleLimits[type]) { + shouldRequest = false; + } + } - this.findSession(true); - return this.mUri; + if (shouldRequest) { + this.mThrottle[type] = now; + } else { + cal.LOG("[calGoogleCalendar] Skipping " + type + " request to reduce requests"); + } + + return shouldRequest; }, - getProperty: function cGC_getProperty(aName) { + getProperty: function(aName) { switch (aName) { + case "googleCalendarName": + return this.mCalendarName; + case "isDefaultCalendar": + return this.isDefaultCalendar; + // Capabilities + case "cache.enabled": + case "cache.always": + return true; case "capabilities.timezones.floating.supported": case "capabilities.attachments.supported": case "capabilities.priority.supported": - case "capabilities.tasks.supported": case "capabilities.alarms.oninvitations.supported": return false; case "capabilities.privacy.values": @@ -245,13 +278,22 @@ return 5; case "capabilities.alarms.actionValues": return ["DISPLAY", "EMAIL", "SMS"]; - case "organizerId": - return "mailto:" + this.googleCalendarName; - case "organizerCN": - if (this.mSession) { - return this.session.fullName; + case "capabilities.tasks.supported": + return !!this.mTasklistName; + case "capabilities.events.supported": + return !!this.mCalendarName; + case "readOnly": + // If this calendar displays events, make it readonly if we are + // not the owner or have write access. + let accessRole = this.getProperty("settings.accessRole"); + let isReader = (accessRole == "freeBusyReader" || accessRole == "reader"); + if (this.mCalendarName && isReader) { + return true; } + // Otherwise fall through break; + case "organizerId": + return "mailto:" + this.mCalendarName; case "itip.transport": if (!this.isDefaultCalendar || !Preferences.get("calendar.google.enableEmailInvitations", false)) { @@ -276,846 +318,446 @@ return this.__proto__.__proto__.getProperty.apply(this, arguments); }, - get canRefresh() { - return true; - }, + setProperty: function(aName, aValue) { + switch (aName) { + case "refreshInterval": + if (aValue < MIN_REFRESH_INTERVAL && aValue != 0) { + cal.LOG("[calGoogleCalendar] Sorry, auto-refresh intervals under " + + MIN_REFRESH_INTERVAL + " minutes would cause the quota " + + "to be reached too fast."); + this.superCalendar.setProperty("refreshInterval", 2 * MIN_REFRESH_INTERVAL); + return; + } + break; + } - adoptItem: function cGC_adoptItem(aItem, aListener) { - cal.LOG("[calGoogleCalendar] Adding item " + aItem.title); + return this.__proto__.__proto__.setProperty.apply(this, arguments); + }, - try { - // Check if calendar is readonly - if (this.readOnly) { - throw new Components.Exception("", - Components.interfaces.calIErrors.CAL_IS_READONLY); + addItemOrUseCache: calendarShim.addItemOrUseCache, + adoptItemOrUseCache: calendarShim.adoptItemOrUseCache, + modifyItemOrUseCache: calendarShim.modifyItemOrUseCache, + deleteItemOrUseCache: calendarShim.deleteItemOrUseCache, + notifyPureOperationComplete: calendarShim.notifyPureOperationComplete, + + addItem: function(aItem, aListener) this.adoptItem(aItem.clone(), aListener), + adoptItem: function(aItem, aListener) { + function stackContains(part, max) { + if (max === undefined) max = 8; + let stack = Components.stack.caller; + while (stack && --max) { + if (stack.filename && stack.filename.endsWith(part)) { + return true; + } + stack = stack.caller; } + return false; + } - // Make sure the item is an event - aItem = aItem.QueryInterface(Components.interfaces.calIEvent); + // Now this sucks...both invitations and the offline cache send over + // items with the id set, but we have no way to figure out which is + // happening just by inspecting the item. Adding offline items should + // not be an import, but invitations should. + let isImport = aItem.id && (aItem.id == "xpcshell-import" || stackContains("calItipUtils.jsm")); + let request = new calGoogleRequest(); - // Check if we have a session. If not, then the user has canceled - // the login prompt. - this.ensureSession(); + Task.spawn(function() { + let itemData = ItemToJSON(aItem, this.offlineStorage, isImport); // Add the calendar to the item, for later use. aItem.calendar = this.superCalendar; - let request = new calGoogleRequest(this); - let xmlEntry = ItemToXMLEntry(aItem, this, - this.session.userName, - this.session.fullName); - request.type = request.ADD; - request.uri = this.fullUri.spec; - request.setUploadData("application/atom+xml; charset=UTF-8", cal.xml.serializeDOM(xmlEntry)); - request.operationListener = aListener; request.calendar = this; - request.newItem = aItem; - request.responseListener = { onResult: this.addItem_response.bind(this) }; - request.addQueryParameter("ctz", calendarDefaultTimezone().tzid); - - this.session.asyncItemRequest(request); - return request; - } catch (e) { - cal.LOG("[calGoogleCalendar] adoptItem failed before request " + aItem.title + "\n:" + e); - if (e.result == Components.interfaces.calIErrors.CAL_IS_READONLY) { - // The calendar is readonly, make sure this is set and - // notify the user. This can come from above or from - // mSession.addItem which checks for the editURI - this.readOnly = true; - } - - this.notifyOperationComplete(aListener, - e.result, - Components.interfaces.calIOperationListener.ADD, - null, - e.message); - } - return null; - }, - - 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) { - cal.LOG("[calGoogleCalendar] Modifying item " + aOldItem.title); - - try { - if (this.readOnly) { - throw new Components.Exception("", - Components.interfaces.calIErrors.CAL_IS_READONLY); - } - - // Check if we have a session. If not, then the user has canceled - // the login prompt. - this.ensureSession(); + if (cal.isEvent(aItem)) { + if (isImport) { + cal.LOG("[calGoogleCalendar] Adding invitation event " + aItem.title); + request.uri = this.createEventsURI("events", "import"); + } else { + cal.LOG("[calGoogleCalendar] Adding regular event " + aItem.title); + request.uri = this.createEventsURI("events"); + } - // Check if enough fields have changed to warrant sending the event - // to google. This saves network traffic. Also check if the item isn't - // the same to work around a bug in the cache layer. - if (aOldItem != aNewItem && relevantFieldsMatch(aOldItem, aNewItem)) { - cal.LOG("[calGoogleCalendar] Not requesting item modification for " + aOldItem.id + - "(" + aOldItem.title + "), relevant fields match"); - - this.notifyOperationComplete(aListener, - Components.results.NS_OK, - Components.interfaces.calIOperationListener.MODIFY, - aNewItem.id, - aNewItem); - this.mObservers.notify("onModifyItem", [aNewItem, aOldItem]); - return null; - } - - // Set up the request - let request = new calGoogleRequest(this.session); - - // We need to clone the new item, its possible that ItemToXMLEntry - // will modify the item. For example, if the item is organized by - // someone else, we cannot save alarms on it and they should - // therefore not be added in the returned item. - let newItem = aNewItem.clone(); - - let xmlEntry = ItemToXMLEntry(newItem, this, - this.session.userName, - this.session.fullName); - - if (aOldItem.parentItem != aOldItem && - !aOldItem.parentItem.recurrenceInfo.getExceptionFor(aOldItem.startDate)) { - - // In this case we are modifying an occurence, not deleting it - request.type = request.ADD; - request.uri = this.fullUri.spec; + if (Preferences.get("calendar.google.sendEventNotifications", false)) { + request.addQueryParameter("sendNotifications", "true"); + } + } else if (cal.isToDo(aItem)) { + cal.LOG("[calGoogleCalendar] Adding task " + aItem.title); + request.uri = this.createTasksURI("tasks"); + // Tasks sent with an id will cause a bad request + delete itemData.id; + } + + if (!request.uri) { + throw Components.Exception("Item type not supported", + Components.results.NS_ERROR_NOT_IMPLEMENTED); + } + + request.setUploadData("application/json; charset=UTF-8", + JSON.stringify(itemData)); + let data = yield this.session.asyncItemRequest(request); + + // All we need to do now is parse the item and complete the + // operation. The cache layer will take care of adding the item + // to the storage. + let defaultTimezone = cal.calendarDefaultTimezone(); + let metaData = Object.create(null); + let item = JSONToItem(data, this, defaultTimezone, + this.defaultReminders || [], + null, metaData); + + // Make sure to update the etag and id + saveItemMetadata(this.offlineStorage, item.hashId, metaData); + + if (aItem.id && item.id != aItem.id) { + // Looks like the id changed, probably because its an offline + // item. This really sucks for us now, because the cache will + // reset the wrong item. As a hack, delete the item with its + // original id and complete the adoptItem call with the new + // item. This will add the new item to the calendar. + let pcal = promisifyCalendar(this.offlineStorage); + yield pcal.deleteItem(aItem); + } + throw new Task.Result(item); + }.bind(this)).then(function(item) { + cal.LOG("[calGoogleCalendar] Adding " + item.title + " succeeded"); + this.observers.notify("onAddItem", [item]); + this.notifyOperationComplete(aListener, Components.results.NS_OK, + cIOL.ADD, item.id, item); + }.bind(this), function(e) { + let code = e.result || Components.results.NS_ERROR_FAILURE; + cal.ERROR("[calGoogleCalendar] Adding Item " + aItem.title + + " failed:" + code + ": " + e.message); + this.notifyPureOperationComplete(aListener, code, cIOL.ADD, aItem.id, e.message); + }.bind(this)); + return request; + }, + + modifyItem: function(aNewItem, aOldItem, aListener) { + cal.LOG("[calGoogleCalendar] Modifying item " + aNewItem.title + " (" + + (aNewItem.recurrenceId ? aNewItem.recurrenceId.icalString : + "master item") + ")"); + + // Set up the request + let request = new calGoogleRequest(); + Task.spawn(function() { + request.type = request.MODIFY; + request.calendar = this; + if (cal.isEvent(aNewItem)) { + request.uri = this.createEventsURI("events", getGoogleId(aNewItem, this.offlineStorage)); + } else if (cal.isToDo(aNewItem)) { + request.uri = this.createTasksURI("tasks", aNewItem.id); + } + + if (!request.uri) { + throw Components.Exception("Item type not supported", + Components.results.NS_ERROR_NOT_IMPLEMENTED); + } + + request.setUploadData("application/json; charset=UTF-8", + JSON.stringify(ItemToJSON(aNewItem, this.offlineStorage))); + + // Set up etag from storage so we don't overwrite any foreign changes + let refItem = aOldItem || aNewItem; + let meta = getItemMetadata(this.offlineStorage, refItem) || + getItemMetadata(this.offlineStorage, refItem.parentItem); + if (meta && meta.etag) { + request.addRequestHeader("If-Match", meta.etag); } else { - // We are making a negative exception or modifying a parent item - request.type = request.MODIFY; - request.uri = getItemEditURI(aOldItem); + cal.ERROR("[calGoogleCalendar] Missing ETag for " + refItem.hashId); } - request.setUploadData("application/atom+xml; charset=UTF-8", cal.xml.serializeDOM(xmlEntry)); - request.responseListener = { onResult: this.modifyItem_response.bind(this) }; - request.operationListener = aListener; - request.newItem = newItem; - request.oldItem = aOldItem; - request.calendar = this; - request.addQueryParameter("ctz", calendarDefaultTimezone().tzid); + let data; + try { + data = yield this.session.asyncItemRequest(request); + } catch (e if e.result == calGoogleRequest.CONFLICT_MODIFY || + e.result == calGoogleRequest.CONFLICT_DELETED) { + data = yield checkResolveConflict(request, this, aNewItem); + } + + // All we need to do now is parse the item and complete the + // operation. The cache layer will take care of adding the item + // to the storage cache. + let defaultTimezone = cal.calendarDefaultTimezone(); + let metaData = Object.create(null); + let item = JSONToItem(data, this, defaultTimezone, + this.defaultReminders || [], + aNewItem.clone(), metaData); + + // Make sure to update the etag. Do so before switching to the + // parent item, as google saves its own etags for changed + // instances. + migrateItemMetadata(this.offlineStorage, aOldItem, item, metaData); + + if (item.recurrenceId) { + // If we only modified an exception item, then we need to + // set the parent item and modify the exception. + let modifiedItem = aNewItem.parentItem.clone(); + if (item.status == "CANCELLED") { + // Canceled means the occurrence is an EXDATE. + modifiedItem.recurrenceInfo.removeOccurrenceAt(item.recurrenceId); + } else { + // Not canceled means the occurrence was modified. + modifiedItem.recurrenceInfo.modifyException(item, true); + } + item = modifiedItem; + } - this.session.asyncItemRequest(request); - return request; - } catch (e) { - cal.LOG("[calGoogleCalendar] modifyItem failed before request " + - aNewItem.title + "(" + aNewItem.id + "):\n" + e); + throw new Task.Result(item); + }.bind(this)).then(function (item) { + cal.LOG("[calGoogleCalendar] Modifying " + aNewItem.title + " succeeded"); + this.observers.notify("onModifyItem", [item, aOldItem]); + this.notifyOperationComplete(aListener, Components.results.NS_OK, + cIOL.MODIFY, item.id, item); - if (e.result == Components.interfaces.calIErrors.CAL_IS_READONLY) { - // The calendar is readonly, make sure this is set and - // notify the user. This can come from above or from - // mSession.modifyItem which checks for the editURI - this.readOnly = true; + }.bind(this), function(e) { + let code = e.result || Components.results.NS_ERROR_FAILURE; + if (code != Components.interfaces.calIErrors.OPERATION_CANCELLED) { + cal.ERROR("[calGoogleCalendar] Modifying item " + aNewItem.title + + " failed:" + code + ": " + e.message); } - - this.notifyOperationComplete(aListener, - e.result, - Components.interfaces.calIOperationListener.MODIFY, - null, - e.message); - } - return null; + this.notifyPureOperationComplete(aListener, code, cIOL.MODIFY, aNewItem.id, e.message); + }.bind(this)); + return request; }, - deleteItem: function cGC_deleteItem(aItem, aListener) { + deleteItem: function(aItem, aListener) { cal.LOG("[calGoogleCalendar] Deleting item " + aItem.title + "(" + aItem.id + ")"); - try { - if (this.readOnly) { - throw new Components.Exception("", - Components.interfaces.calIErrors.CAL_IS_READONLY); - } - - // Check if we have a session. If not, then the user has canceled - // the login prompt. - this.ensureSession(); - - // 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. - let request = new calGoogleRequest(this); - + let request = new calGoogleRequest(); + Task.spawn(function() { request.type = request.DELETE; - request.uri = getItemEditURI(aItem); - request.operationListener = aListener; - request.oldItem = aItem; request.calendar = this; - request.responseListener = { onResult: this.deleteItem_response.bind(this) }; - - this.session.asyncItemRequest(request); - return request; - } catch (e) { - cal.LOG("[calGoogleCalendar] deleteItem failed before request for " + - aItem.title + "(" + aItem.id + "):\n" + e); - - if (e.result == Components.interfaces.calIErrors.CAL_IS_READONLY) { - // The calendar is readonly, make sure this is set and - // notify the user. This can come from above or from - // mSession.deleteItem which checks for the editURI - this.readOnly = true; + if (cal.isEvent(aItem)) { + request.uri = this.createEventsURI("events", getGoogleId(aItem, this.offlineStorage)); + if (Preferences.get("calendar.google.sendEventNotifications", false)) { + request.addQueryParameter("sendNotifications", "true"); + } + } else if (cal.isToDo(aItem)) { + request.uri = this.createTasksURI("tasks", aItem.id); } - this.notifyOperationComplete(aListener, - e.result, - Components.interfaces.calIOperationListener.DELETE, - null, - e.message); - } - return null; - }, - - getItem: function cGC_getItem(aId, aListener) { - // This function needs a test case using mechanisms in bug 365212 - cal.LOG("[calGoogleCalendar] Getting item with id " + aId); - try { - - // Check if we have a session. If not, then the user has canceled - // the login prompt. - this.ensureSession(); - - // Set up the request - - let request = new calGoogleRequest(this); + if (!request.uri) { + throw Components.Exception("Item type not supported", + Components.results.NS_ERROR_NOT_IMPLEMENTED); + } - request.itemId = aId; - request.type = request.GET; - request.uri = this.fullUri.spec; - request.operationListener = aListener; - request.responseListener = { onResult: this.getItem_response.bind(this) }; - request.calendar = this; + // Set up etag from storage so we don't overwrite any foreign changes + let meta = getItemMetadata(this.offlineStorage, aItem) || + getItemMetadata(this.offlineStorage, aItem.parentItem); + if (meta && meta.etag) { + request.addRequestHeader("If-Match", meta.etag); + } else { + cal.ERROR("[calGoogleCalendar] Missing ETag for " + aItem.hashId); + } - // Request Parameters - request.addQueryParameter("ctz", calendarDefaultTimezone().tzid); - request.addQueryParameter("max-results", kMANY_EVENTS); - request.addQueryParameter("singleevents", "false"); - - this.session.asyncItemRequest(request); - return request; - } catch (e) { - cal.LOG("[calGoogleCalendar] getItem failed before request " + aId + "):\n" + e); - - this.notifyOperationComplete(aListener, - e.result, - Components.interfaces.calIOperationListener.GET, - null, - e.message); - } - return null; - }, - - 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. - this.ensureSession(); + try { + yield this.session.asyncItemRequest(request); + } catch (e if e.result == calGoogleRequest.CONFLICT_MODIFY || + e.result == calGoogleRequest.CONFLICT_DELETED) { + yield checkResolveConflict(request, this, aItem); + } - // item base type - let wantEvents = ((aItemFilter & - Components.interfaces.calICalendar.ITEM_FILTER_TYPE_EVENT) != 0); - let wantInvitations = ((aItemFilter & - Components.interfaces.calICalendar.ITEM_FILTER_REQUEST_NEEDS_ACTION) != 0); - - if (!wantEvents) { - // Events are not wanted, nothing to do. The - // notifyOperationComplete in the catch block below will catch - // this. - throw new Components.Exception("", Components.results.NS_OK); - } - - // Requesting only a DATE returns items based on UTC. Therefore, we make - // sure both start and end dates include a time and timezone. This may - // not quite be what was requested, but I'd say its a shortcoming of - // rfc3339. - if (aRangeStart) { - aRangeStart = aRangeStart.clone(); - aRangeStart.isDate = false; - } - if (aRangeEnd) { - aRangeEnd = aRangeEnd.clone(); - aRangeEnd.isDate = false; - } - - let rfcRangeStart = cal.toRFC3339(aRangeStart); - let rfcRangeEnd = cal.toRFC3339(aRangeEnd); - - let request = new calGoogleRequest(this); - - request.type = request.GET; - request.uri = this.fullUri.spec; - request.operationListener = aListener; - request.calendar = this; - request.itemRangeStart = aRangeStart; - request.itemRangeEnd = aRangeEnd; - request.itemFilter = aItemFilter; - - // Request Parameters - request.addQueryParameter("ctz", calendarDefaultTimezone().tzid); - request.addQueryParameter("max-results", - aCount ? aCount : kMANY_EVENTS); - request.addQueryParameter("singleevents", "false"); - request.addQueryParameter("start-min", rfcRangeStart); - request.addQueryParameter("start-max", rfcRangeEnd); - request.responseListener = { onResult: this.getItems_response.bind(this) }; - this.session.asyncItemRequest(request); - return request; - } catch (e) { - this.notifyOperationComplete(aListener, - e.result, - Components.interfaces.calIOperationListener.GET, - null, - e.message); - } - return null; - }, + deleteItemMetadata(this.offlineStorage, aItem); - refresh: function cGC_refresh() { - this.mObservers.notify("onLoad", [this]); + throw new Task.Result(aItem); + }.bind(this)).then(function (item) { + cal.LOG("[calGoogleCalendar] Deleting " + aItem.title + " succeeded"); + this.observers.notify("onDeleteItem", [item]); + this.notifyOperationComplete(aListener, Components.results.NS_OK, + cIOL.DELETE, item.id, item); + }.bind(this), function(e) { + let code = e.result || Components.results.NS_ERROR_FAILURE; + if (code != Components.interfaces.calIErrors.OPERATION_CANCELLED) { + cal.ERROR("[calGoogleCalendar] Deleting item " + aItem.title + + " failed:" + code + ": " + e.message); + } + this.notifyPureOperationComplete(aListener, code, cIOL.DELETE, aItem.id, e.message); + }.bind(this)); + return request; }, - /* - * Google Calendar Provider Response Listener functions - */ - - /** - * addItem_response - * Response function, called by the session object when an item was added - * - * @param aOperation The calIGoogleRequest processing the request - * @param aData In case of an error, this is the error string, otherwise - * an XML representation of the added item. - */ - addItem_response: function cGC_addItem_response(aOperation, aData) { - // First, have the general response retrieve the item - let item, e; - try { - item = DataToItem(aOperation, aData, this, null); - if (!resolveConflicts(aOperation, item)) { - // If a conflict occurred and the user wants to overwrite, a new - // request will be sent. bail out here, this method will be - // called again - return; - } - } catch (exp) { - item = null; - e = exp; - } - - let resultCode; - if (item) { - cal.LOG("[calGoogleCalendar] Adding item " + item.title + " successful"); - this.mObservers.notify("onAddItem", [item]); - resultCode = Components.results.NS_OK; - } else { - cal.LOG("[calGoogleCalendar] Adding item " + aOperation.newItem.id + " failed, status " + aOperation.status + ", Exception: " + e); - resultCode = isCacheException(e) ? Components.results.NS_ERROR_NOT_AVAILABLE : e.result || Components.results.NS_ERROR_FAILURE; - } - - this.notifyOperationComplete(aOperation.operationListener, - resultCode, - Components.interfaces.calIOperationListener.ADD, - (item ? item.id : null), - (item ? item : e.message)); + getItem: function(aId, aListener) { + this.mOfflineStorage.getItem.apply(this.mOfflineStorage, arguments); }, - /** - * modifyItem_response - * Response function, called by the session object when an item was modified - * - * @param aOperation The calIGoogleRequest processing the request - * @param aData In case of an error, this is the error string, otherwise - * an XML representation of the added item. - */ - modifyItem_response: function cGC_modifyItem_response_onResult(aOperation, - aData) { - let self = this; - function notifyObserver(item, oldItem) { - if (item && item.parentItem != item) { - item.parentItem.recurrenceInfo.modifyException(item, false); - item = item.parentItem; - oldItem = oldItem.parentItem; - } - // Notify Observers - if (item) { - self.mObservers.notify("onModifyItem", [item, oldItem]); - } - } - - // First, convert the data to an item and make sure no conflicts occurred. - let newItem, e; - try { - newItem = DataToItem(aOperation, aData, this, aOperation.newItem); - if (!resolveConflicts(aOperation, newItem)) { - // If a conflict occurred and the user wants to overwrite, a new - // request will be sent. bail out here, this method will be - // called again - return; - } - } catch (exp) { - newItem = null; - e = exp; - } - - let resultCode; - if (newItem) { - cal.LOG("[calGoogleCalendar] Modifying item " + newItem.id + " successful"); - notifyObserver(newItem, aOperation.oldItem); - resultCode = Components.results.NS_OK; - } else { - cal.LOG("[calGoogleCalendar] Modifying item " + aOperation.oldItem.id + " failed, status " + aOperation.status + ", Exception: " + e); - resultCode = isCacheException(e) ? Components.results.NS_ERROR_NOT_AVAILABLE : e.result || Components.results.NS_ERROR_FAILURE; - } - this.notifyOperationComplete(aOperation.operationListener, - resultCode, - Components.interfaces.calIOperationListener.MODIFY, - (newItem ? newItem.id : null), - (newItem ? newItem : e.message)); + getItems: function(aFilter, aCount, aRangeStart, aRangeEnd, aListener) { + this.mOfflineStorage.getItems.apply(this.mOfflineStorage, arguments); }, - /** - * deleteItem_response - * Response function, called by the session object when an Item was deleted - * - * @param aOperation The calIGoogleRequest processing the request - * @param aData In case of an error, this is the error string, otherwise - * an XML representation of the added item. - */ - deleteItem_response: function cGC_deleteItem_response_onResult(aOperation, - aData) { - let item, e; - try { - item = DataToItem(aOperation, aData, this, aOperation.oldItem); - if (!resolveConflicts(aOperation, item)) { - // If a conflict occurred and the user wants to overwrite, a new - // request will be sent. bail out here, this method will be - // called again - return; - } - } catch (exp) { - item = null; - e = exp; - } - - let resultCode; - if (item) { - cal.LOG("[calGoogleCalendar] Deleting item " + aOperation.oldItem.id + " successful"); - this.mObservers.notify("onDeleteItem", [item]); - resultCode = Components.results.NS_OK; - } else { - cal.LOG("[calGoogleCalendar] Deleting item " + aOperation.oldItem.id + " failed, status " + aOperation.status + ", Exception: " + e); - resultCode = isCacheException(e) ? Components.results.NS_ERROR_NOT_AVAILABLE : e.result || Components.results.NS_ERROR_FAILURE; - } - this.notifyOperationComplete(aOperation.operationListener, - resultCode, - Components.interfaces.calIOperationListener.DELETE, - (item ? item.id : null), - (item ? item : e.message)); + refresh: function() { + this.mObservers.notify("onLoad", [this]); }, /** - * getItem_response - * Response function, called by the session object when a single Item was - * downloaded. - * - * @param aOperation The calIGoogleRequest processing the request - * @param aData In case of an error, this is the error string, otherwise - * an XML representation of the added item. + * Implement calIChangeLog */ - getItem_response: function cGC_getItem_response_onResult(aOperation, - aData) { - // XXX Due to google issue 399, we need to parse a full feed here. - try { - if (!Components.isSuccessCode(aOperation.status)) { - throw new Components.Exception(aData, aOperation.status); - } - - // A feed was passed back, parse it. - let xml = cal.xml.parseString(aData); - let timezoneString = gdataXPathFirst(xml, 'atom:feed/gCal:timezone/@value') || "UTC"; - let timezone = gdataTimezoneService.getTimezone(timezoneString); - - // 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. - this.ensureSession(); - - // Get the item entry by id. - let itemXPath = 'atom:feed/atom:entry[substring-before(atom:id/text(), "' + aOperation.itemId + '")!="" or gCal:uid/@value="' + aOperation.itemId + '"]'; - let itemEntry = gdataXPathFirst(xml, itemXPath); - if (!itemEntry) { - // Item wasn't found. Skip onGetResult and just complete. Not - // finding an item isn't a user-important error, it may be a - // wanted case. (i.e itip) - cal.LOG("[calGoogleCalendar] Item " + aOperation.itemId + " not found in calendar " + this.name); - throw new Components.Exception("Item not found", Components.results.NS_OK); - } - let item = XMLEntryToItem(itemEntry, timezone, this); - item.calendar = this.superCalendar; - - if (item.recurrenceInfo) { - // If this item is recurring, get all exceptions for this item. - for each (let entry in gdataXPath(xml, 'atom:feed/atom:entry[gd:originalEvent/@id="' + aOperation.itemId + '"]')) { - let excItem = XMLEntryToItem(entry, timezone, this); - - // Google uses the status field to reflect negative - // exceptions. - if (excItem.status == "CANCELED") { - item.recurrenceInfo.removeOccurrenceAt(excItem.recurrenceId); + get offlineStorage() this.mOfflineStorage, + set offlineStorage(val) { + this.mOfflineStorage = val; + let cacheVersion = this.getProperty("cache.version"); + let resetPromise; + if (cacheVersion && cacheVersion != this.CACHE_DB_VERSION) { + cal.LOG("[calGoogleCalendar] Migrating cache from " + + cacheVersion + " to " + this.CACHE_DB_VERSION); + this.resetSync() + resetPromise = this.resetSync(); + } else if (!cacheVersion) { + resetPromise = Promise.resolve(); + } + + if (resetPromise) { + resetPromise.then(function() { + this.setProperty("cache.version", this.CACHE_DB_VERSION); + }.bind(this)); + } + + return val; + }, + + resetLog: function() { + this.resetSync().then(function() { + this.mObservers.notify("onLoad", [this]); + }.bind(this)); + }, + + resetSync: function() { + return new Promise(function(resolve, reject) { + cal.LOG("[calGoogleCalendar] Resetting last updated counter for " + this.name); + this.setProperty("syncToken.events", ""); + this.setProperty("lastUpdated.tasks", ""); + this.mThrottle = Object.create(null); + this.mOfflineStorage.QueryInterface(Components.interfaces.calICalendarProvider) + .deleteCalendar(this.mOfflineStorage, { + onDeleteCalendar: function(aCalendar, aStatus, aDetal) { + if (Components.isSuccessCode(aStatus)) { + resolve(); } else { - excItem.calendar = this; - item.recurrenceInfo.modifyException(excItem, true); + reject(aDetail); } } - } - // We are done, notify the listener of our result and that we are - // done. - cal.LOG("[calGoogleCalendar] Item " + aOperation.itemId + " was found in calendar " + this.name); - aOperation.operationListener.onGetResult(this.superCalendar, - Components.results.NS_OK, - Components.interfaces.calIEvent, - null, - 1, - [item]); - this.notifyOperationComplete(aOperation.operationListener, - Components.results.NS_OK, - Components.interfaces.calIOperationListener.GET, - item.id, - null); - } catch (e) { - if (!Components.isSuccessCode(e.result)) { - cal.LOG("[calGoogleCalendar] Error getting item " + aOperation.itemId + ":\n" + e); - Components.utils.reportError(e); - } - this.notifyOperationComplete(aOperation.operationListener, - e.result, - Components.interfaces.calIOperationListener.GET, - null, - e.message); - } + }); + }); }, - /** - * getItems_response - * Response function, called by the session object when an Item feed was - * downloaded. - * - * @param aOperation The calIGoogleRequest processing the request - * @param aData In case of an error, this is the error string, otherwise - * an XML representation of the added item. - */ - getItems_response: function cGC_getItems_response(aOperation, aData) { - // To simplify code, provide a one-stop function to call, independant of - // if and what type of listener was passed. - let listener = aOperation.operationListener || - { onGetResult: function() {}, onOperationComplete: function() {} }; - - cal.LOG("[calGoogleCalendar] Received response for " + aOperation.uri); - try { - // Check if the call succeeded - if (!Components.isSuccessCode(aOperation.status)) { - throw new Components.Exception(aData, aOperation.status); - } - - // A feed was passed back, parse it. - let xml = cal.xml.parseString(aData); - let timezoneString = gdataXPathFirst(xml, 'atom:feed/gCal:timezone/@value') || "UTC"; - let timezone = gdataTimezoneService.getTimezone(timezoneString); - - // 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. - this.ensureSession(); - - if (gdataXPathFirst(xml, 'atom:feed/atom:author/atom:email/text()') == this.mSession.userName) { - // If the current entry contains the user's email, then we can - // extract the user's full name also. - this.mSession.fullName = gdataXPathFirst(xml, 'atom:feed/atom:author/atom:name/text()'); - } - - let wantInvitations = ((aOperation.itemFilter & - Components.interfaces.calICalendar.ITEM_FILTER_REQUEST_NEEDS_ACTION) != 0); - - // Parse all tags - for each (let entry in gdataXPath(xml, 'atom:feed/atom:entry')) { - if (gdataXPathFirst(entry, 'gd:originalEvent')) { - // This is an exception. If we are doing an uncached - // operation, then skip it for now since it will be parsed - // later. - continue; - } - - let item = XMLEntryToItem(entry, timezone, this); - item.calendar = this.superCalendar; - - if (wantInvitations) { - // If invitations are wanted and this is not an invitation, - // or if the user is not an attendee, or has already accepted - // then this is not an invitation. - let att = item.getAttendeeById("mailto:" + this.session.userName); - if (!this.isInvitation(item) || - !att || - att.participationStatus != "NEEDS-ACTION") { - continue; - } + replayChangesOn: function(aListener) { + // Figure out if the user is idle, no need to synchronize if so. + let idleTime = Components.classes["@mozilla.org/widget/idleservice;1"] + .getService(Components.interfaces.nsIIdleService) + .idleTime; + let maxIdleTime = Preferences.get("calendar.google.idleTime", 300) * 1000; + + if (maxIdleTime != 0 && idleTime > maxIdleTime) { + cal.LOG("[calGoogleCalendar] Skipping refresh since user is idle"); + aListener.onResult({ status: Components.results.NS_OK }, null); + return Promise.resolve(); + } + + // Now that we've determined we are not idle we can continue with the sync. + let maxResults = Preferences.get("calendar.google.maxResultsPerRequest", null); + + // We are going to be making potentially lots of changes to the offline + // storage, start a batch operation. + this.mOfflineStorage.startBatch(); + + // Update the calendar settings + let calendarPromise = Promise.resolve(); + if (this.mCalendarName && this.checkThrottle("calendarList")) { + let calendarRequest = new calGoogleRequest(); + calendarRequest.calendar = this; + calendarRequest.type = calendarRequest.GET; + calendarRequest.uri = this.createUsersURI("calendarList", this.mCalendarName) + calendarPromise = this.session.asyncItemRequest(calendarRequest).then(function(aData) { + if (aData.defaultReminders) { + this.defaultReminders = aData.defaultReminders.map(function(x) JSONToAlarm(x, true)); + } else { + this.defaultReminders = []; } - cal.LOG("[calGoogleCalendar] Parsing entry:\n" + cal.xml.serializeDOM(entry) + "\n"); - - if (item.recurrenceInfo) { - // If we are doing an uncached operation, then we need to - // gather all exceptions and put them into the item. - // Otherwise, our listener will take care of mapping the - // exception to the base item. - for each (let oid in gdataXPath(xml, 'atom:feed/atom:entry[gd:originalEvent/@id="' + item.id + '"]')) { - // Get specific fields so we can speed up the parsing process - let status = gdataXPathFirst(oid, 'gd:eventStatus/@value').substring(39); - - if (status == "canceled") { - let rId = gdataXPathFirst(oid, 'gd:when/@startTime'); - let rDate = cal.fromRFC3339(rId, timezone); - cal.LOG("[calGoogleCalendar] Negative exception " + rId + "/" + rDate); - item.recurrenceInfo.removeOccurrenceAt(rDate); - } else { - // Parse the exception and modify the current item - let excItem = XMLEntryToItem(oid, timezone, this); - - if (excItem) { - // Google uses the status field to reflect negative - // exceptions. - excItem.calendar = this; - item.recurrenceInfo.modifyException(excItem, true); - } - } - } + for each (let k in ["accessRole", "backgroundColor", "description", + "foregroundColor", "location", "primary", + "summary", "summaryOverride", "timeZone"]) { + this.setProperty("settings." + k, aData[k]); } - - item.makeImmutable(); - LOGitem(item); - - // This is an uncached call, expand the item and tell our - // get listener about the item. - let expandedItems = expandItems(item, aOperation); - listener.onGetResult(this.superCalendar, - Components.results.NS_OK, - Components.interfaces.calIEvent, - null, - expandedItems.length, - expandedItems); - } - // Operation Completed successfully. - this.notifyOperationComplete(listener, - Components.results.NS_OK, - Components.interfaces.calIOperationListener.GET, - null, - null); - } catch (e) { - cal.LOG("[calGoogleCalendar] Error getting items:\n" + e); - this.notifyOperationComplete(listener, - e.result, - Components.interfaces.calIOperationListener.GET, - null, - e.message); - } - }, - - /** - * Implement calIChangeLog - */ - get offlineStorage() { - return this.mOfflineStorage; - }, - - set offlineStorage(val) { - return (this.mOfflineStorage = val); - }, - - resetLog: function cGC_resetLog() { - cal.LOG("[calGoogleCalendar] Resetting last updated counter for " + this.name); - this.deleteProperty("google.lastUpdated"); - }, - - replayChangesOn: function cGC_replayChangesOn(aListener) { - let lastUpdate = this.getProperty("google.lastUpdated"); - let lastUpdateDateTime; - if (lastUpdate) { - // Set up the last sync stamp - lastUpdateDateTime = createDateTime(); - lastUpdateDateTime.icalString = lastUpdate; - - // Set up last week - let lastWeek = getCorrectedDate(now().getInTimezone(UTC())); - lastWeek.day -= 7; - if (lastWeek.compare(lastUpdateDateTime) >= 0) { - // The last sync was longer than a week ago. Google requires a full - // sync in that case. This call also takes care of calling - // resetLog(). - this.superCalendar.wrappedJSObject.setupCachedCalendar(); - lastUpdateDateTime = null; - } - cal.LOG("[calGoogleCalendar] The calendar " + this.name + " was last modified: " + lastUpdateDateTime); - } - - let request = new calGoogleRequest(this.mSession); - - request.type = request.GET; - request.uri = this.fullUri.spec - request.destinationCal = this.mOfflineStorage; - - let calendar = this; - request.responseListener = { onResult: this.syncItems_response.bind(this, (lastUpdateDateTime == null)) } - request.operationListener = aListener; - request.calendar = this; - - // Request Parameters - request.addQueryParameter("ctz", calendarDefaultTimezone().tzid); - request.addQueryParameter("max-results", kMANY_EVENTS); - request.addQueryParameter("singleevents", "false"); - - if (lastUpdateDateTime) { - // Partial sync requires sending updated-min - request.addQueryParameter("updated-min", cal.toRFC3339(lastUpdateDateTime)); + this.setProperty("settings.defaultReminders", JSON.stringify(aData.defaultReminders)); + }.bind(this)); } - // Request the item. The response function is ready to take care of both - // uncached getItem requests and this type of synchronization request. - this.mSession.asyncItemRequest(request); - }, - - /** - * syncItems_response - * Response function, called by the session object when an Item feed was - * downloaded. - * - * @param aIsFullSync If set, this is a full sync rather than an update. - * @param aOperation The calIGoogleRequest processing the request - * @param aData In case of an error, this is the error string, otherwise - * an XML representation of the added item. - */ - syncItems_response: function cGC_syncItems_response(aIsFullSync, aOperation, aData) { - cal.LOG("[calGoogleCalendar] Received response for " + aOperation.uri + (aIsFullSync ? " (full sync)" : "")); - try { - // Check if the call succeeded - if (!Components.isSuccessCode(aOperation.status)) { - throw new Components.Exception(aData, aOperation.status); - } - - // A feed was passed back, parse it. - let xml = cal.xml.parseString(aData); - let timezoneString = gdataXPathFirst(xml, 'atom:feed/gCal:timezone/@value') || "UTC"; - let timezone = gdataTimezoneService.getTimezone(timezoneString); - - // 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. - this.ensureSession(); - - if (gdataXPathFirst(xml, 'atom:feed/atom:author/atom:email/text()') == this.mSession.userName) { - // If the current entry contains the user's email, then we can - // extract the user's full name also. - this.mSession.fullName = gdataXPathFirst(xml, 'atom:feed/atom:author/atom:name/text()'); - } - - // This is the calendar we should sync changes into. - let destinationCal = aOperation.destinationCal; - - for each (let entry in gdataXPath(xml, 'atom:feed/atom:entry')) { - let recurrenceId = getRecurrenceIdFromEntry(entry, timezone); - if (aIsFullSync && recurrenceId) { - // On a full sync, we parse exceptions different. - continue; - } - cal.LOG("[calGoogleCalendar] Parsing entry:\n" + entry + "\n"); - - let referenceItemObj = {} - destinationCal.getItem(getIdFromEntry(entry), - new syncSetter(referenceItemObj)); - let referenceItem = referenceItemObj.value && - referenceItemObj.value.clone(); - - // Parse the item. If we got a reference item from the storage - // calendar, put that in to make sure we get all exceptions and - // such. - let item = XMLEntryToItem(entry, - timezone, - this, - (recurrenceId && referenceItem ? null : referenceItem)); - item.calendar = this.superCalendar; - - if (aIsFullSync && item.recurrenceInfo) { - // On a full synchronization, we can go ahead and pre-parse - // all exceptions and then add the item at once. This way we - // make sure - for each (let oid in gdataXPath(xml, 'atom:feed/atom:entry[gd:originalEvent/@id="' + item.id + '"]')) { - // Get specific fields so we can speed up the parsing process - let status = gdataXPathFirst(oid, 'gd:eventStatus/@value').substring(39); - - if (status == "canceled") { - let rId = gdataXPathFirst(oid, 'gd:when/@startTime'); - let rDate = cal.fromRFC3339(rId, timezone); - item.recurrenceInfo.removeOccurrenceAt(rDate); - cal.LOG("[calGoogleCalendar] Negative exception " + rId + "/" + rDate); - } else { - // Parse the exception and modify the current item - let excItem = XMLEntryToItem(oid, timezone, this); - - if (excItem) { - // Google uses the status field to reflect negative - // exceptions. - excItem.calendar = this; - item.recurrenceInfo.modifyException(excItem, true); - } - } - } + // Set up a request for the events + let eventsRequest = new calGoogleRequest(); + let eventsPromise = Promise.resolve(); + eventsRequest.calendar = this; + eventsRequest.type = eventsRequest.GET; + eventsRequest.uri = this.createEventsURI("events"); + eventsRequest.addQueryParameter("timeZone", cal.calendarDefaultTimezone().tzid); + eventsRequest.addQueryParameter("maxResults", maxResults); + let syncToken = this.getProperty("syncToken.events"); + if (syncToken) { + eventsRequest.addQueryParameter("showDeleted", "true"); + eventsRequest.addQueryParameter("syncToken", syncToken); + } + if (eventsRequest.uri && this.checkThrottle("events")) { + let saver = new ItemSaver(this); + eventsPromise = this.session.asyncPaginatedRequest(eventsRequest, null, function(aData) { + // On each request... + saver.parseItemStream(aData); + }.bind(this), function(aData) { + // On last request... + saver.processRemainingExceptions(); + + if (aData.nextSyncToken) { + cal.LOG("[calGoogleCalendar] New sync token for " + + this.name + "(events) is now: " + aData.nextSyncToken); + this.setProperty("syncToken.events", aData.nextSyncToken); } + }.bind(this)); + } - LOGitem(item); - - if (!aIsFullSync && item.recurrenceId && referenceItem) { - // This is a single occurrence that has been updated. - if (item.status == "CANCELED") { - // Canceled means the occurrence is an EXDATE. - referenceItem.recurrenceInfo.removeOccurrenceAt(item.recurrenceId); - } else { - // Not canceled means the occurrence was modified. - item.parentItem = referenceItem; - referenceItem.recurrenceInfo.modifyException(item, true); - } - destinationCal.modifyItem(referenceItem, null, null); - } else if (!item.recurrenceId) { - // This is a normal item. If it was canceled, then it should - // be deleted, otherwise it should be either added or - // modified. The relaxed mode of the destination calendar - // takes care of the latter two cases. - if (item.status == "CANCELED") { - destinationCal.deleteItem(item, null); - } else { - destinationCal.modifyItem(item, null, null); - } - } else { - // We could not find the parent item for the occurrence in - // the feed. - WARN("occurrence without parent for item " + item.id); - } + // Set up a request for tasks + let tasksRequest = new calGoogleRequest(); + let tasksPromise = Promise.resolve(); + tasksRequest.calendar = this; + tasksRequest.type = tasksRequest.GET; + tasksRequest.uri = this.createTasksURI("tasks"); + tasksRequest.addQueryParameter("maxResults", maxResults); + let lastUpdated = this.getUpdatedMin("tasks"); + if (lastUpdated) { + tasksRequest.addQueryParameter("updatedMin", cal.toRFC3339(lastUpdated)); + tasksRequest.addQueryParameter("showDeleted", "true"); + } + if (tasksRequest.uri && this.checkThrottle("tasks")) { + let saver = new ItemSaver(this); + tasksPromise = this.session.asyncPaginatedRequest(tasksRequest, function(aData) { + // On the first request... + cal.LOG("[calGoogleCalendar] Last sync date for " + this.name + + "(tasks) is now: " + tasksRequest.requestDate.toString()); + let lastUpdated = tasksRequest.requestDate.icalString; + this.setProperty("lastUpdated.tasks", lastUpdated); + }.bind(this), function(aData) { + // On each request... + saver.parseItemStream(aData); + }.bind(this)); + } + + return PromiseAll([calendarPromise, eventsPromise, tasksPromise]).then(function() { + this.mOfflineStorage.endBatch(); + aListener.onResult({ status: Components.results.NS_OK }, null); + }.bind(this), function(e) { + this.mOfflineStorage.endBatch(); + let code = e.result || Components.results.NS_ERROR_FAILURE; + if (code == calGoogleRequest.RESOURCE_GONE) { + cal.LOG("[calGoogleCalendar] Server did not accept " + + "incremental update, resetting calendar and " + + "starting over."); + this.resetSync().then(function() { + this.replayChangesOn(aListener); + }.bind(this)); + } else { + cal.LOG("[calGoogleCalendar] Error syncing:\n" + code + ":" + + stringException(e)); + aListener.onResult({ status: code }, e.message); } - - // Set the last updated timestamp to now. - cal.LOG("[calGoogleCalendar] Last sync date for " + this.name + " is now: " + - aOperation.requestDate.toString()); - this.setProperty("google.lastUpdated", - aOperation.requestDate.icalString); - - // Tell our listener we are done. - aOperation.operationListener.onResult(aOperation, null); - } catch (e) { - cal.LOG("[calGoogleCalendar] Error syncing items:\n" + e); - aOperation.operationListener.onResult({ status: e.result }, e.message); - } + }.bind(this)); }, /** @@ -1123,7 +765,7 @@ * provider, but we want to advertise that we will always take care of * notifications. */ - canNotify: function cGC_canNotify(aMethod, aItem) { - return true; - } + canNotify: function(aMethod, aItem) true }; + +var NSGetFactory = XPCOMUtils.generateNSGetFactory([calGoogleCalendar]); diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleCalendar.manifest thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleCalendar.manifest --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleCalendar.manifest 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleCalendar.manifest 2014-12-15 12:28:32.000000000 +0000 @@ -0,0 +1,2 @@ +component {d1a6e988-4b4d-45a5-ba46-43e501ea96e3} calGoogleCalendar.js +contract @mozilla.org/calendar/calendar;1?type=gdata {d1a6e988-4b4d-45a5-ba46-43e501ea96e3} diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleCalendarModule.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleCalendarModule.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleCalendarModule.js 2014-12-08 19:30:40.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleCalendarModule.js 1970-01-01 00:00:00.000000000 +0000 @@ -1,49 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -Components.utils.import("resource://gre/modules/Services.jsm"); -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); -Components.utils.import("resource://calendar/modules/calUtils.jsm"); - -// These constants are used internally to signal errors, to avoid the need for -// our own error range in calIErrors -const kGOOGLE_LOGIN_FAILED = 1; -const kGOOGLE_CONFLICT_DELETED = 2; -const kGOOGLE_CONFLICT_MODIFY = 3 - -/** Module Registration */ -const calendarScriptLoadOrder = [ - "calUtils.js", -]; - -const gdataScriptLoadOrder = [ - "calGoogleCalendar.js", - "calGoogleSession.js", - "calGoogleRequest.js", - "calGoogleUtils.js" -]; - -function NSGetFactory(cid) { - if (!this.scriptsLoaded) { - // First load the calendar scripts - cal.loadScripts(calendarScriptLoadOrder, Components.utils.getGlobalForObject(this)); - - // Now load gdata extension scripts. __LOCATION__ is the current - // filename, so __LOCATION__.parent == . We expect to find the - // subscripts in ./../js - let thisDir = __LOCATION__.parent.parent.clone(); - thisDir.append("js"); - cal.loadScripts(gdataScriptLoadOrder, Components.utils.getGlobalForObject(this), thisDir); - this.scriptsLoaded = true; - } - - let components = [ - calGoogleCalendar, - calGoogleSession, - calGoogleSessionManager, - calGoogleRequest - ]; - - return (XPCOMUtils.generateNSGetFactory(components))(cid); -} diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleCalendarModule.manifest thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleCalendarModule.manifest --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleCalendarModule.manifest 2014-12-08 19:30:40.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleCalendarModule.manifest 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -component {d1a6e988-4b4d-45a5-ba46-43e501ea96e3} calGoogleCalendarModule.js -contract @mozilla.org/calendar/calendar;1?type=gdata {d1a6e988-4b4d-45a5-ba46-43e501ea96e3} - -component {652f6233-e03f-438a-bd3b-39877f68c0f4} calGoogleCalendarModule.js -contract @mozilla.org/calendar/providers/gdata/session;1 {652f6233-e03f-438a-bd3b-39877f68c0f4} - -component {6a7ba1f0-f271-49b0-8e93-5ca33651b4af} calGoogleCalendarModule.js -contract @mozilla.org/calendar/providers/gdata/session-manager;1 {6a7ba1f0-f271-49b0-8e93-5ca33651b4af} - -component {53a3438a-21bc-4a0f-b813-77a8b4f19282} calGoogleCalendarModule.js -contract @mozilla.org/calendar/providers/gdata/request;1 {53a3438a-21bc-4a0f-b813-77a8b4f19282} diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleRequest.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleRequest.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleRequest.js 2014-12-08 19:30:40.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleRequest.js 1970-01-01 00:00:00.000000000 +0000 @@ -1,420 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -Components.utils.import("resource://calendar/modules/calUtils.jsm"); -Components.utils.import("resource://gre/modules/Services.jsm"); -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); -Components.utils.import("resource://gre/modules/Preferences.jsm"); - -/** - * calGoogleRequest - * This class represents a HTTP request sent to Google - * - * @constructor - * @class - */ -function calGoogleRequest(aSession) { - this.mQueryParameters = new Array(); - this.mSession = aSession; - this.wrappedJSObject = this; -} -const calGoogleRequestClassID = Components.ID("{53a3438a-21bc-4a0f-b813-77a8b4f19282}"); -const calGoogleRequestInterfaces = [ - Components.interfaces.calIGoogleRequest, - Components.interfaces.calIOperation, - Components.interfaces.nsIStreamLoaderObserver, - Components.interfaces.nsIInterfaceRequestor, - Components.interfaces.nsIChannelEventSink -]; -calGoogleRequest.prototype = { - - /* Members */ - mUploadContent: null, - mUploadData: null, - mSession: null, - mExtraData: null, - mQueryParameters: null, - mType: null, - mCalendar: null, - mLoader: null, - mStatus: Components.results.NS_OK, - - /* Constants */ - LOGIN: 0, - ADD: 1, - MODIFY: 2, - DELETE: 3, - GET: 4, - - /* Simple Attributes */ - method: "GET", - id: null, - uri: null, - responseListener: null, - operationListener: null, - reauthenticate: true, - - itemRangeStart: null, - itemRangeEnd: null, - itemFilter: null, - itemId: null, - calendar: null, - newItem: null, - oldItem: null, - destinationCal: null, - - classID: calGoogleRequestClassID, - QueryInterface: XPCOMUtils.generateQI(calGoogleRequestInterfaces), - classInfo: XPCOMUtils.generateCI({ - classID: calGoogleRequestClassID, - contractID: "@mozilla.org/calendar/providers/gdata/request;1", - classDescription: "Google Calendar Request", - interfaces: calGoogleRequestInterfaces - }), - - /** - * Implement calIOperation - */ - get isPending() { - return (this.mLoader && this.mLoader.request != null); - }, - - get status() { - if (this.isPending) { - return this.mLoader.request.status; - } else { - return this.mStatus; - } - }, - - cancel: function cGR_cancel(aStatus) { - if (this.isPending) { - this.mLoader.request.cancel(aStatus); - this.mStatus = aStatus; - } - }, - - /** - * attribute type - * The type of this reqest. Must be one of - * LOGIN, GET, ADD, MODIFY, DELETE - * This also sets the Request Method and for the LOGIN request also the uri - */ - get type() { return this.mType; }, - - set type(v) { - switch (v) { - case this.LOGIN: - this.method = "POST"; - this.uri = "https://www.google.com/accounts/ClientLogin"; - break; - case this.GET: - this.method = "GET"; - break; - case this.ADD: - this.method = "POST"; - break; - case this.MODIFY: - this.method = "PUT"; - break; - case this.DELETE: - this.method = "DELETE"; - break; - default: - throw new Components.Exception("", Components.results.NS_ERROR_ILLEGAL_VALUE); - break; - } - this.mType = v; - return v; - }, - - /** - * setUploadData - * The HTTP body data for a POST or PUT request. - * - * @param aContentType The Content type of the Data - * @param aData The Data to upload - */ - setUploadData: function cGR_setUploadData(aContentType, aData) { - this.mUploadContent = aContentType; - this.mUploadData = aData; - }, - - /** - * addQueryParameter - * Adds a query parameter to this request. This will be used in conjunction - * with the uri. - * - * @param aKey The key of the request parameter. - * @param aValue The value of the request parameter. This parameter will - * be escaped. - */ - addQueryParameter: function cGR_addQueryParameter(aKey, aValue) { - if (aValue == null || aValue == "") { - // Silently ignore empty values. - return; - } - this.mQueryParameters.push(aKey + "=" + encodeURIComponent(aValue)); - }, - - /** - * commit - * Starts the request process. This can be called multiple times if the - * request should be repeated - * - * @param aSession The session object this request should be made with. - * This parameter is optional - */ - commit: function cGR_commit(aSession) { - - try { - // Set the session to request with - if (aSession) { - this.mSession = aSession; - } - - let uristring = this.uri; - if (this.mQueryParameters.length > 0) { - uristring += "?" + this.mQueryParameters.join("&"); - } - let uri = Services.io.newURI(uristring, null, null); - let channel = Services.io.newChannelFromURI(uri); - - this.prepareChannel(channel); - - channel = channel.QueryInterface(Components.interfaces.nsIHttpChannel); - channel.redirectionLimit = 3; - - this.mLoader = cal.createStreamLoader(); - - cal.LOG("[calGoogleCalendar] Requesting " + this.method + " " + - channel.URI.spec); - - channel.notificationCallbacks = this; - - cal.sendHttpRequest(this.mLoader, channel, this); - } catch (e) { - // Let the response function handle the error that happens here - this.fail(e.result, e.message); - } - }, - - /** - * fail - * Call this request's listener with the given code and Message - * - * @param aCode The Error code to fail with - * @param aMessage The Error message. If this is null, an error Message - * from calIGoogleErrors will be used. - */ - fail: function cGR_fail(aCode, aMessage) { - this.mLoader = null; - this.mStatus = aCode; - this.responseListener.onResult(this, aMessage); - }, - - /** - * succeed - * Call this request's listener with a Success Code and the given Result. - * - * @param aResult The result Text of this request. - */ - succeed: function cGR_succeed(aResult) { - // Succeeding is nothing more than failing with the result code set to - // NS_OK. - this.fail(Components.results.NS_OK, aResult); - }, - - /** - * prepareChannel - * Prepares the passed channel to match this objects properties - * - * @param aChannel The Channel to be prepared - */ - prepareChannel: function cGR_prepareChannel(aChannel) { - - // No caching - aChannel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE; - - // Set upload Data - if (this.mUploadData) { - let converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]. - createInstance(Components.interfaces.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - - let stream = converter.convertToInputStream(this.mUploadData); - aChannel = aChannel.QueryInterface(Components.interfaces.nsIUploadChannel); - aChannel.setUploadStream(stream, this.mUploadContent, -1); - - if (this.mType == this.LOGIN) { - cal.LOG("[calGoogleCalendar] Setting upload data for login request (hidden)"); - } else { - cal.LOG("[calGoogleCalendar] Setting Upload Data (" + - this.mUploadContent + "):\n" + this.mUploadData); - } - } - - aChannel = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel); - - // Depending on the preference, we will use X-HTTP-Method-Override to - // get around some proxies. This will default to true. - if (Preferences.get("calendar.google.useHTTPMethodOverride", true) && - (this.method == "PUT" || this.method == "DELETE")) { - - aChannel.requestMethod = "POST"; - aChannel.setRequestHeader("X-HTTP-Method-Override", - this.method, - false); - if (this.method == "DELETE") { - // DELETE has no body, set an empty one so that Google accepts - // the request. - aChannel.setRequestHeader("Content-Type", - "application/atom+xml; charset=UTF-8", - false); - aChannel.setRequestHeader("Content-Length", 0, false); - } - } else { - aChannel.requestMethod = this.method; - } - - // Add Authorization - if (this.mSession.authToken) { - aChannel.setRequestHeader("Authorization", - "GoogleLogin auth=" - + this.mSession.authToken, - false); - } - }, - - /** - * @see nsIInterfaceRequestor - * @see calProviderUtils.jsm - */ - getInterface: cal.InterfaceRequestor_getInterface, - - /** - * @see nsIChannelEventSink - */ - asyncOnChannelRedirect: function cGR_onChannelRedirect(aOldChannel, - aNewChannel, - aFlags, - aCallback) { - // all we need to do to the new channel is the basic preparation - this.prepareChannel(aNewChannel); - aCallback.onRedirectVerifyCallback(Components.results.NS_OK); - }, - - /** - * @see nsIStreamLoaderObserver - */ - onStreamComplete: function cGR_onStreamComplete(aLoader, - aContext, - aStatus, - aResultLength, - aResult) { - if (!aResult || !Components.isSuccessCode(aStatus)) { - this.fail(aStatus, aResult); - return; - } - - let httpChannel = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel); - - // Convert the stream, falling back to utf-8 in case its not given. - let result = cal.convertByteArray(aResult, aResultLength, httpChannel.contentCharset); - if (result === null) { - this.fail(Components.results.NS_ERROR_FAILURE, - "Could not convert bytestream to Unicode: " + e); - return; - } - - // Calculate Google Clock Skew - let serverDate = new Date(httpChannel.getResponseHeader("Date")); - let curDate = new Date(); - - // The utility function getCorrectedDate in calGoogleUtils.js receives - // its clock skew seconds from here. The clock skew is updated on each - // request and is therefore quite accurate. - getCorrectedDate.mClockSkew = curDate.getTime() - serverDate.getTime(); - - // Remember when this request happened - this.requestDate = cal.jsDateToDateTime(serverDate); - - // Handle all (documented) error codes - switch (httpChannel.responseStatus) { - case 200: /* No error. */ - case 201: /* Creation of a resource was successful. */ - // Everything worked out, we are done - this.succeed(result); - break; - - case 401: /* Authorization required. */ - case 403: /* Unsupported standard parameter, or authentication or - Authorization failed. */ - cal.LOG("[calGoogleCalendar] Login failed for " + this.mSession.userName + - " HTTP Status " + httpChannel.responseStatus); - - // login failed. auth token must be invalid, password too - - if (this.type == this.MODIFY || - this.type == this.DELETE || - this.type == this.ADD) { - // Encountering this error on a write request means the - // calendar is readonly - this.fail(Components.interfaces.calIErrors.CAL_IS_READONLY, result); - } else if (!this.reauthenticate) { - // If no reauth was requested, then don't invalidate the - // whole session and just bail out - this.fail(kGOOGLE_LOGIN_FAILED, result); - } else if (this.type == this.LOGIN) { - // If this was a login request itself, then fail it. - // That will take care of logging in again - this.mSession.invalidate(); - this.fail(kGOOGLE_LOGIN_FAILED, result); - } else { - // Retry the request. Invalidating the session will trigger - // a new login dialog. - this.mSession.invalidate(); - this.mSession.asyncItemRequest(this); - } - - break; - case 404: /* The resource was not found on the server, which is - also a conflict */ - // 404 NOT FOUND: Resource (such as a feed or entry) not found. - this.fail(kGOOGLE_CONFLICT_DELETED, ""); - break; - case 409: /* Specified version number doesn't match resource's - latest version number. */ - this.fail(kGOOGLE_CONFLICT_MODIFY, result); - break; - case 400: - // 400 BAD REQUEST: Invalid request URI or header, or - // unsupported nonstandard parameter. - - // HACK Ugh, this sucks. If we send a lower sequence number, Google - // will throw a 400 and not a 409, even though both cases are a conflict. - if (result == "Cannot decrease the sequence number of an event") { - this.fail(kGOOGLE_CONFLICT_MODIFY, "SEQUENCE-HACK"); - break; - } - - // Otherwise fall through - default: - // The following codes are caught here: - // 500 INTERNAL SERVER ERROR: Internal error. This is the - // default code that is used for - // all unrecognized errors. - // - - // Something else went wrong - let error = "A request Error Occurred. Status Code: " + - httpChannel.responseStatus + " " + - httpChannel.responseStatusText + " Body: " + - result; - - this.fail(Components.results.NS_ERROR_NOT_AVAILABLE, error); - break; - } - } -}; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleSession.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleSession.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleSession.js 2014-12-08 19:30:40.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleSession.js 1970-01-01 00:00:00.000000000 +0000 @@ -1,577 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -Components.utils.import("resource://calendar/modules/calUtils.jsm"); -Components.utils.import("resource://calendar/modules/calXMLUtils.jsm"); -Components.utils.import("resource://calendar/modules/calProviderUtils.jsm"); -Components.utils.import("resource://gre/modules/Services.jsm"); -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - -// This constant is an arbitrary large number. It is used to tell google to get -// many events, the exact number is not important. -const kMANY_EVENTS = 0x7FFFFFFF; - -function calGoogleSessionManager() { - this.wrappedJSObject = this; - -} -const calGoogleSessionManagerClassID = Components.ID("{6a7ba1f0-f271-49b0-8e93-5ca33651b4af}"); -const calGoogleSessionManagerInterfaces = [Components.interfaces.calIGoogleSessionManager]; -calGoogleSessionManager.prototype = { - mSessionMap: {}, - - classID: calGoogleSessionManagerClassID, - QueryInterface: XPCOMUtils.generateQI(calGoogleSessionManagerInterfaces), - classInfo: XPCOMUtils.generateCI({ - classID: calGoogleSessionManagerClassID, - contractID: "@mozilla.org/calendar/providers/gdata/session-manager;1", - classDescription: "Google Calendar Session Manager", - interfaces: calGoogleSessionManagerInterfaces, - flags: Components.interfaces.nsIClassInfo.SINGLETON - }), - - /** - * getSessionByUsername - * Get a Session object for the specified username. If aCreate is false, - * null will be returned if the session doesn't exist. Otherwise, the - * session will be created. - * - * @param aUsername The username to get the session for - * @param aCreate If true, the session will be created prior to returning - */ - getSessionByUsername: function cGSM_getSessionByUsername(aUsername, aCreate) { - // If the username contains no @, assume @gmail.com - // XXX Maybe use accountType=GOOGLE and just pass the raw username? - if (aUsername.indexOf('@') == -1) { - aUsername += "@gmail.com"; - } - - // Check if the session exists - if (!this.mSessionMap.hasOwnProperty(aUsername)) { - if (!aCreate) { - return null; - } - cal.LOG("[calGoogleCalendar] Creating session for: " + aUsername); - this.mSessionMap[aUsername] = new calGoogleSession(aUsername); - } else { - cal.LOG("[calGoogleCalendar] Reusing session for: " + aUsername); - } - - // XXX What happens if the username is "toSource" :) - return this.mSessionMap[aUsername]; - } -}; - -/** - * calGoogleSession - * This Implements a Session object to communicate with google - * - * @constructor - * @class - */ -function calGoogleSession(aUsername) { - this.mItemQueue = new Array(); - this.mGoogleUser = aUsername; - this.wrappedJSObject = this; - - // Register a freebusy provider for this session - getFreeBusyService().addProvider(this); -} -const calGoogleSessionClassID = Components.ID("{652f6233-e03f-438a-bd3b-39877f68c0f4}"); -const calGoogleSessionInterfaces = [Components.interfaces.calIGoogleSession]; -calGoogleSession.prototype = { - classID: calGoogleSessionClassID, - QueryInterface: XPCOMUtils.generateQI(calGoogleSessionInterfaces), - classInfo: XPCOMUtils.generateCI({ - classID: calGoogleSessionClassID, - contractID: "@mozilla.org/calendar/providers/gdata/session;1", - classDescription: "Google Calendar Session", - interfaces: calGoogleSessionInterfaces - }), - - /* Member Variables */ - mGoogleUser: null, - // This must be |undefined|, we need this variable to be tri-state. - mGooglePass: undefined, - mGoogleFullName: null, - mAuthToken: null, - mSessionID: null, - - mLoggingIn: false, - mPersistPassword: false, - mItemQueue: null, - - mCalendarName: null, - - /** - * readonly attribute authToken - * - * The auth token returned from Google Accounts - */ - get authToken() { - return this.mAuthToken; - }, - - /** - * readonly attribute userName - * - * The username for this session. To get a session with a different - * username, use calIGoogleSessionManager. - */ - get userName() { - return this.mGoogleUser; - }, - - /** - * attribute persist - * - * If set, the password will persist across restarts. - */ - get persist() { - return this.mPersistPassword; - }, - set persist(v) { - return this.mPersistPassword = v; - }, - - /** - * attribute AUTF8String fullName - * - * The user's full name, usually retrieved from the XML fields. - */ - get fullName() { - return this.mGoogleFullName; - }, - set fullName(v) { - return this.mGoogleFullName = v; - }, - - /** - * attribute AUTF8String password - * - * The password used to authenticate. It is only important to implement the - * setter here, since the password is only used internally. - */ - get password() { - return this.mGooglePass; - }, - set password(v) { - return this.mGooglePass = v; - }, - - /** - * invalidate - * Resets the Auth token and password. - */ - invalidate: function cGS_invalidate() { - this.mAuthToken = null; - this.mGooglePass = null; - this.persist = false; - - passwordManagerRemove(this.mGoogleUser); - }, - - getCalendars: function cGS_getCalendars(aListener) { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - - /** - * failQueue - * Fails all requests in this session's queue. Optionally only fail requests - * for a certain calendar - * - * @param aCode Failure Code - * @param aCalendar The calendar to fail for. Can be null. - */ - failQueue: function cGS_failQueue(aCode, aCalendar) { - function cGS_failQueue_failInQueue(element, index, arr) { - if (!aCalendar || (aCalendar && element.calendar == aCalendar)) { - element.fail(aCode, null); - return false; - - } - return true; - } - - this.mItemQueue = this.mItemQueue.filter(cGS_failQueue_failInQueue); - }, - - /** - * loginAndContinue - * Prepares a login request, then requests via #asyncRawRequest - * - * - * @param aCalendar The calendar of the request that initiated the login. - */ - loginAndContinue: function cGS_loginAndContinue(aCalendar) { - if (this.mLoggingIn) { - cal.LOG("[calGoogleCalendar] loginAndContinue called while logging in"); - return; - } - try { - cal.LOG("[calGoogleCalendar] Logging in to " + this.mGoogleUser); - - // We need to have a user and should not be logging in - ASSERT(!this.mLoggingIn); - ASSERT(this.mGoogleUser); - - // Start logging in - this.mLoggingIn = true; - - if (this.mGooglePass === undefined) { - // This happens only on the first run. Try to get the password - // from the password manager. - let password = {}; - passwordManagerGet(this.mGoogleUser, password); - if (password.value) { - cal.LOG("Retrieved Password for " + this.mGoogleUser + - " from password manager"); - this.mPersistPassword = true; - this.mGooglePass = password.value; - } else { - // Could not get the password from the password manager, set - // it to null to make sure the password is prompted for in - // the next block. - this.mGooglePass = null; - } - } - - // Check if we have a password. If not, authentication may have - // failed. - if (!this.mGooglePass) { - let username = { value: this.mGoogleUser }; - let password = { value: null }; - let persist = { value: false }; - - // Try getting a new password, potentially switching sesssions. - let calendarName = (aCalendar ? - aCalendar.googleCalendarName : - this.mGoogleUser); - - if (getCalendarCredentials(calendarName, - username, - password, - persist)) { - - cal.LOG("[calGoogleCalendar] Got the pw from the calendar credentials: " + - calendarName); - - // If a different username was entered, switch sessions - - if (aCalendar && username.value != this.mGoogleUser) { - let newSession = getGoogleSessionManager() - .getSessionByUsername(username.value, - true); - newSession.password = password.value; - newSession.persist = persist.value; - setCalendarPref(aCalendar, - "googleUser", - "CHAR", - username.value); - - // Set the new session for the calendar - aCalendar.session = newSession; - cal.LOG("[calGoogleCalendar] Setting " + aCalendar.name + - "'s Session to " + newSession.userName); - - // Move all requests by this calendar to its new session - function cGS_login_moveToSession(element, index, arr) { - if (element.calendar == aCalendar) { - cal.LOG("[calGoogleCalendar] Moving " + element.uri + " to " + - newSession.userName); - newSession.asyncItemRequest(element); - return false; - } - return true; - } - this.mItemQueue = this.mItemQueue - .filter(cGS_login_moveToSession); - - // Do not complete the request here, since it has been - // moved. This is not an error though, so nothing is - // thrown. - return; - } - - // If we arrive at this point, then the session was not - // changed. Just adapt the password from the dialog and - // continue. - this.mGooglePass = password.value; - this.persist = persist.value; - } else { - cal.LOG("[calGoogleCalendar] Could not get any credentials for " + - calendarName + " (" + - this.mGoogleUser + ")"); - - if (aCalendar) { - // First of all, disable the calendar so no further login - // dialogs show up. - aCalendar.setProperty("disabled", true); - aCalendar.setProperty("auto-enabled", true); - - // Unset the session in the requesting calendar, if the user - // canceled the login dialog that also asks for the - // username, then the session is not valid. This also - // prevents multiple login windows. - aCalendar.session = null; - } - - // We are done logging in - this.mLoggingIn = false; - - // The User even canceled the login prompt asking for - // the user. This means we have to fail all requests - // that belong to that calendar and are in the queue. This - // will also include the request that initiated the login - // request, so that dosent need to be handled extra. If no - // calendar was passed, fail all request in that queue - this.failQueue(Components.results.NS_ERROR_NOT_AVAILABLE, - aCalendar); - return; - } - } - - // Now we should have a password - ASSERT(this.mGooglePass); - - // Get Version info - let source = Services.appinfo.vendor + "-" + - Services.appinfo.name + "-" + - Services.appinfo.version; - - // Request Login - let request = new calGoogleRequest(this); - - request.type = request.LOGIN; - request.calendar = aCalendar; - request.responseListener = this; - - request.setUploadData("application/x-www-form-urlencoded", - "Email=" + encodeURIComponent(this.mGoogleUser) + - "&Passwd=" + encodeURIComponent(this.mGooglePass) + - "&accountType=HOSTED_OR_GOOGLE" + - "&source=" + encodeURIComponent(source) + - "&service=cl"); - this.asyncRawRequest(request); - } catch (e) { - // If something went wrong, reset the login state just in case - this.mLoggingIn = false; - cal.LOG("[calGoogleCalendar] Error Logging In: " + e); - - // If something went wrong, then this.loginComplete should handle - // the error. We don't need to take care of the request that - // initiated the login, since it is also in the item queue. - this.onResult({ status: e.result}, e.message); - } - }, - - /** - * onResult (loginComplete) - * Callback function that is called when the login request to Google - * Accounts has finished - * - Retrieves the Authentication Token - * - Saves the Password in the Password Manager - * - Processes the Item Queue - * - * @private - * @param aOperation The calIOperation that initiated the login - * @param aData The (String) Result of the Request - * (or an Error Message) - */ - onResult: function cGS_onResult(aOperation, aData) { - // About mLoggingIn: this should only be set to false when either - // something went wrong or mAuthToken is set. This avoids redundant - // logins to Google. Hence mLoggingIn is set three times in the course - // of this function - - if (!aData || !Components.isSuccessCode(aOperation.status)) { - this.mLoggingIn = false; - cal.LOG("[calGoogleCalendar] Login failed. Status: " + aOperation.status); - - if (aOperation.status == kGOOGLE_LOGIN_FAILED && - aOperation.reauthenticate) { - // If the login failed, then retry the login. This is not an - // error that should trigger failing the calICalendar's request. - this.loginAndContinue(aOperation.calendar); - } else { - cal.LOG("[calGoogleCalendar] Failing queue with " + aOperation.status); - this.failQueue(aOperation.status); - } - } else { - let start = aData.indexOf("Auth="); - if (start == -1) { - // The Auth token could not be extracted - this.mLoggingIn = false; - this.invalidate(); - - // Retry login - this.loginAndContinue(aOperation.calendar); - } else { - this.mAuthToken = aData.substring(start + 5, aData.length - 1); - - this.mLoggingIn = false; - - if (this.persist) { - try { - passwordManagerSave(this.mGoogleUser, - this.mGooglePass); - } catch (e) { - // This error is non-fatal, but would constrict - // functionality - cal.LOG("[calGoogleCalendar] Error adding password to manager"); - } - } - - // Process Items that were requested while logging in - let request; - // Extra parentheses to avoid js strict warning. - while ((request = this.mItemQueue.shift())) { - cal.LOG("[calGoogleCalendar] Processing Queue Item: " + request.uri); - request.commit(this); - } - } - } - }, - - /** - * asyncItemRequest - * get or post an Item from or to Google using the Queue. - * - * @param aRequest The Request Object. This is an instance of - * calGoogleRequest - */ - asyncItemRequest: function cGS_asyncItemRequest(aRequest) { - - if (!this.mLoggingIn && this.mAuthToken) { - // We are not currently logging in and we have an auth token, so - // directly try the login request - this.asyncRawRequest(aRequest); - } else { - // Push the request in the queue to be executed later - this.mItemQueue.push(aRequest); - - cal.LOG("[calGoogleCalendar] Adding item " + aRequest.uri + " to queue"); - - // If we are logging in, then we are done since the passed request - // will be processed when the login is complete. Otherwise start - // logging in. - if (!this.mLoggingIn && this.mAuthToken == null) { - // We need to do this on a timeout, otherwise the UI thread will - // block when the password prompt is shown. - setTimeout(function() { - this.loginAndContinue(aRequest.calendar); - }, 0, this); - } - } - }, - - /** - * asyncRawRequest - * get or post an Item from or to Google without the Queue. - * - * @param aRequest The Request Object. This is an instance of - * calGoogleRequest - */ - asyncRawRequest: function cGS_asyncRawRequest(aRequest) { - // Request is handled by an instance of the calGoogleRequest - // We don't need to keep track of these requests, they - // pass to a listener or just die - - ASSERT(aRequest); - aRequest.commit(this); - }, - - /** - * calIFreeBusyProvider Implementation - */ - getFreeBusyIntervals: function cGS_getFreeBusyIntervals(aCalId, - aRangeStart, - aRangeEnd, - aBusyTypes, - aListener) { - if (aCalId.indexOf("@") < 0 || aCalId.indexOf(".") < 0) { - // No valid email, screw it - aListener.onResult(null, null); - return null; - } - - // Requesting only a DATE returns items based on UTC. Therefore, we make - // sure both start and end dates include a time and timezone. This may - // not quite be what was requested, but I'd say its a shortcoming of - // rfc3339. - if (aRangeStart) { - aRangeStart = aRangeStart.clone(); - aRangeStart.isDate = false; - } - if (aRangeEnd) { - aRangeEnd = aRangeEnd.clone(); - aRangeEnd.isDate = false; - } - - let rfcRangeStart = cal.toRFC3339(aRangeStart); - let rfcRangeEnd = cal.toRFC3339(aRangeEnd); - - let request = new calGoogleRequest(this); - - request.type = request.GET; - request.uri = "https://www.google.com/calendar/feeds/" + - encodeURIComponent(aCalId.replace(/^mailto:/i, "")) + - "/private/free-busy"; - request.operationListener = aListener; - request.itemRangeStart = aRangeStart; - request.itemRangeEnd = aRangeEnd; - request.reauthenticate = false; - - // Request Parameters - request.addQueryParameter("ctz", calendarDefaultTimezone().tzid); - request.addQueryParameter("max-results", kMANY_EVENTS); - request.addQueryParameter("singleevents", "true"); - request.addQueryParameter("start-min", rfcRangeStart); - request.addQueryParameter("start-max", rfcRangeEnd); - - let session = this; - request.responseListener = { - onResult: function cGS_getFreeBusyIntervals_onResult(aOperation, aData) { - session.getFreeBusyIntervals_response(aOperation, - aData, - aCalId, - aRangeStart, - aRangeEnd); - } - }; - - this.asyncItemRequest(request); - return request; - }, - - getFreeBusyIntervals_response: function getFreeBusyIntervals_response(aOperation, - aData, - aCalId, - aRangeStart, - aRangeEnd) { - // Prepare Namespaces - if (aOperation.status == kGOOGLE_LOGIN_FAILED || - !Components.isSuccessCode(aOperation.status)) { - aOperation.operationListener.onResult(aOperation, null); - return; - } - - // A feed was passed back, parse it. - let xml = cal.xml.parseString(aData); - let timezoneString = gdataXPathFirst(xml, 'atom:feed/gCal:timezone/@value') || "UTC"; - let timezone = gdataTimezoneService.getTimezone(timezoneString); - - let intervals = []; - const fbtypes = Components.interfaces.calIFreeBusyInterval; - for each (let entry in gdataXPath(xml, 'atom:feed/atom:entry')) { - let start = cal.fromRFC3339(gdataXPathFirst(entry, 'gd:when/@startTime'), timezone); - let end = cal.fromRFC3339(gdataXPathFirst(entry, 'gd:when/@endTime'), timezone); - let interval = new cal.FreeBusyInterval(aCalId, fbtypes.BUSY, start, end); - LOGinterval(interval); - intervals.push(interval); - } - - aOperation.operationListener.onResult(aOperation, intervals); - } -}; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleUtils.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleUtils.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/calGoogleUtils.js 2014-12-08 19:30:40.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/calGoogleUtils.js 1970-01-01 00:00:00.000000000 +0000 @@ -1,1312 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -Components.utils.import("resource://calendar/modules/calUtils.jsm"); -Components.utils.import("resource://calendar/modules/calXMLUtils.jsm"); -Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm"); -Components.utils.import("resource://calendar/modules/calProviderUtils.jsm"); -Components.utils.import("resource://gre/modules/Preferences.jsm"); - -const atomNS = "http://www.w3.org/2005/Atom"; -const gdNS = "http://schemas.google.com/g/2005"; -const gcalNS = "http://schemas.google.com/gCal/2005"; - -function gdataNSResolver(prefix) { - const ns = { - atom: atomNS, - gd: gdNS, - gCal: gcalNS - }; - - return ns[prefix] || atomNS; -} - -function gdataXPath(aNode, aExpr, aType) { - return cal.xml.evalXPath(aNode, aExpr, gdataNSResolver, aType); -} -function gdataXPathFirst(aNode, aExpr, aType) { - // Different than the caldav/ics functions, this one will return an empty string on null - return cal.xml.evalXPathFirst(aNode, aExpr, gdataNSResolver, aType) || ""; -} - -/** - * getGoogleSessionManager - * Shortcut to the google session manager - */ -function getGoogleSessionManager() { - if (this.mObject === undefined) { - this.mObject = - Components.classes["@mozilla.org/calendar/providers/gdata/session-manager;1"] - .createInstance(Components.interfaces.calIGoogleSessionManager); - } - return this.mObject; -} - -/** - * setCalendarPref - * Helper to set an independant Calendar Preference, since I cannot use the - * calendar manager because of early initialization Problems. - * - * @param aCalendar The Calendar to set the pref for - * @param aPrefName The Preference name - * @param aPrefType The type of the preference ("BOOL", "INT", "CHAR") - * @param aPrefValue The Preference value - * - * @return The value of aPrefValue - * - * @require aCalendar.googleCalendarName - */ -function setCalendarPref(aCalendar, aPrefName, aPrefType, aPrefValue) { - - Preferences.set("calendar.google.calPrefs." + aCalendar.googleCalendarName + "." + - aPrefName, aPrefValue, aPrefType); - - return aPrefValue; -} - -/** - * getCalendarPref - * Helper to get an independant Calendar Preference, since I cannot use the - * calendar manager because of early initialization Problems. - * - * @param aCalendar The calendar to set the pref for - * @param aPrefName The preference name - * - * @return The preference value - * - * @require aCalendar.googleCalendarName - */ -function getCalendarPref(aCalendar, aPrefName) { - return Preferences.get("calendar.google.calPrefs." + - aCalendar.googleCalendarName + "." + aPrefName); -} - -/** - * getFormattedString - * Returns the string from the properties file, formatted with args - * - * @param aBundleName The .properties file to access - * @param aStringName The property to access - * @param aFormatArgs An array of arguments to format the string - * @param aComponent Optionally, the stringbundle component name - * @return The formatted string - */ -function getFormattedString(aBundleName, aStringName, aFormatArgs, aComponent) { - return calGetString(aBundleName, aStringName, aFormatArgs, aComponent || "gdata-provider"); -} - -/** - * getCalendarCredentials - * Tries to get the username/password combination of a specific calendar name - * from the password manager or asks the user. - * - * @param in aCalendarName The calendar name to look up. Can be null. - * @param out aUsername The username that belongs to the calendar. - * @param out aPassword The password that belongs to the calendar. - * @param out aSavePassword Should the password be saved? - * @return Could a password be retrieved? - */ -function getCalendarCredentials(aCalendarName, - aUsername, - aPassword, - aSavePassword) { - return cal.auth.getCredentials("Google Calendar", - aCalendarName, - aUsername, - aPassword, - aSavePassword); -} - -/** - * Gets the date and time that Google's http server last sent us. Note the - * passed argument is modified. This might not be the exact server time (i.e it - * may be off by network latency), but it does give a good guess when syncing. - * - * @param aDate The date to modify - */ -function getCorrectedDate(aDate) { - - if (!getCorrectedDate.mClockSkew) { - return aDate; - } - - aDate.second += getCorrectedDate.mClockSkew; - return aDate; -} - -/** - * The timezone service to translate Google timezones. - */ -var gdataTimezoneService = { - ctz: cal.getTimezoneService(), - - get floating() { - return this.ctz.floating; - }, - - get UTC() { - return this.ctz.UTC; - }, - - get version() { - return this.ctz.version; - }, - - get defaultTimezone() { - return this.ctz.defaultTimezone; - }, - - getTimezone: function gTS_getTimezone(aTzid) { - if (aTzid == "Etc/GMT") { - // Most timezones are covered by the timezone service, there is one - // exception I've found out about. GMT without DST is pretty close - // to UTC, lets take it. - return UTC(); - } - - let baseTZ = this.ctz.getTimezone(aTzid); - ASSERT(baseTZ, "Unknown Timezone requested: " + aTzid); - return baseTZ; - } -}; - -/** - * passwordManagerSave - * Helper to insert an entry to the password manager. - * - * @param aUserName The username to search - * @param aPassword The corresponding password - */ -function passwordManagerSave(aUsername, aPassword) { - cal.auth.passwordManagerSave(aUsername, aPassword, aUsername, "Google Calendar"); -} - -/** - * passwordManagerGet - * Helper to retrieve an entry from the password manager - * - * @param in aUsername The username to search - * @param out aPassword The corresponding password - * @return Does an entry exist in the password manager - */ -function passwordManagerGet(aUsername, aPassword) { - return cal.auth.passwordManagerGet(aUsername, aPassword, aUsername, "Google Calendar"); -} - -/** - * passwordManagerRemove - * Helper to remove an entry from the password manager - * - * @param aUsername The username to remove. - * @return Could the user be removed? - */ -function passwordManagerRemove(aUsername) { - return cal.auth.passwordManagerRemove(aUsername, aUsername, "Google Calendar"); -} - -/** - * If the operation has signaled that a conflict occurred, then prompt the user - * to overwrite. If the user chooses to overwrite, restart the request with the - * right parameters so the request succeeds. - * - * @param aOperation The operation to check - * @param aItem The updated item from the response - * @return False if further processing should be cancelled - */ -function resolveConflicts(aOperation, aItem) { - if (aItem && (aOperation.status == kGOOGLE_CONFLICT_DELETED || - aOperation.status == kGOOGLE_CONFLICT_MODIFY)) { - if (aItem == "SEQUENCE-HACK") { - // Working around a Google issue here, see what happens on a 400 - // code in calGoogleRequest.js. This will cause a new request - // without the sequence number. In return, we get a new item with - // the correct sequence number. - let newItem = aOperation.newItem.clone(); - let session = aOperation.calendar.session; - newItem.deleteProperty("SEQUENCE"); - let xmlEntry = ItemToXMLEntry(newItem, aOperation.calendar, - session.userName, session.fullName); - - aOperation.newItem = newItem; - aOperation.setUploadData("application/atom+xml; charset=UTF-8", cal.xml.serializeDOM(xmlEntry)); - session.asyncItemRequest(aOperation); - return false; - } else if (aOperation.status == kGOOGLE_CONFLICT_DELETED && - aOperation.type == aOperation.DELETE) { - // Deleted on the server and deleted locally. Great! - return true; - } else { - // If a conflict occurred, then prompt - let method = (aOperation.type == aOperation.DELETE ? "delete" : "modify") - let inputItem = aOperation.oldItem || aOperation.newItem; - let overwrite = cal.promptOverwrite(method, inputItem); - if (overwrite) { - if (aOperation.status == kGOOGLE_CONFLICT_DELETED && - aOperation.type == aOperation.MODIFY) { - // The item was deleted on the server, but modified locally. - // Add it again - aOperation.type = aOperation.ADD; - aOperation.uri = aOperation.calendar.fullUri.spec; - aOperation.calendar.session.asyncItemRequest(aOperation); - return false; - } else if (aOperation.status == kGOOGLE_CONFLICT_MODIFY && - aOperation.type == aOperation.MODIFY) { - // The item was modified in both places, repeat the current - // request with the edit uri of the updated event - aOperation.uri = getItemEditURI(aItem); - aOperation.calendar.session.asyncItemRequest(aOperation); - return false; - } else if (aOperation.status == kGOOGLE_CONFLICT_MODIFY && - aOperation.type == aOperation.DELETE) { - // Modified on the server, deleted locally. Just repeat the - // delete request with the updated edit uri. - aOperation.uri = getItemEditURI(aItem); - aOperation.calendar.session.asyncItemRequest(aOperation); - return false; - } - } - } - // Otherwise, we can just continue using the item that was parsed, it - // is the newest version on the server. - } - return true; -} - -/** - * Helper function to convert raw data directly into a calIItemBase. If the - * passed operation signals an error, then throw an exception - * - * @param aOperation The operation to check for errors - * @param aData The result from the response - * @param aGoogleCalendar The calIGoogleCalendar to operate on - * @param aReferenceItem The reference item to apply the information to - * @return The parsed item - * @throws An exception on a parsing or request error - */ -function DataToItem(aOperation, aData, aGoogleCalendar, aReferenceItem) { - if (aOperation.status == kGOOGLE_CONFLICT_DELETED || - aOperation.status == kGOOGLE_CONFLICT_MODIFY || - Components.isSuccessCode(aOperation.status)) { - - let item; - if (aData == "SEQUENCE-HACK") { - // Working around a Google issue here, see what happens on a 400 - // code in calGoogleRequest.js. This will be processed in - // resolveConflicts(). - return "SEQUENCE-HACK"; - } - - if (aData && aData.length) { - let xml = cal.xml.parseString(aData); - cal.LOG("[calGoogleCalendar] Parsing entry:\n" + aData + "\n"); - - // Get the local timezone from the preferences - let timezone = calendarDefaultTimezone(); - - // Parse the Item with the given timezone - item = XMLEntryToItem(xml.documentElement, timezone, - aGoogleCalendar, - aReferenceItem); - } else { - cal.LOG("[calGoogleCalendar] No content, using reference item instead "); - // No data happens for example on delete. Just assume the reference - // item. - item = aReferenceItem.clone(); - } - - LOGitem(item); - item.calendar = aGoogleCalendar.superCalendar; - return item; - } else { - throw new Components.Exception(aData, aOperation.status); - } -} - -/** - * ItemToXMLEntry - * Converts a calIEvent to a string of xml data. - * - * @param aItem The item to convert - * @param aCalendar The calendar to use, this must be a calIGoogleCalendar - * @param aAuthorEmail The email of the author of the event - * @param aAuthorName The full name of the author of the event - * @return The xml data of the item - */ -function ItemToXMLEntry(aItem, aCalendar, aAuthorEmail, aAuthorName) { - let selfIsOrganizer = (!aItem.organizer || - aItem.organizer.id == "mailto:" + aCalendar.googleCalendarName); - - function addExtendedProperty(aName, aValue) { - if (!selfIsOrganizer || !aValue) { - // We can't set extended properties if we are not the organizer, - // discard. Also, if the value is null/false, we can delete the - // extended property by not adding it. - return; - } - let gdExtendedProp = document.createElementNS(gdNS, "extendedProperty"); - gdExtendedProp.setAttribute("name", aName); - gdExtendedProp.setAttribute("value", aValue || ""); - entry.appendChild(gdExtendedProp); - } - - if (!aItem) { - throw new Components.Exception("", Components.results.NS_ERROR_INVALID_ARG); - } - - const kEVENT_SCHEMA = "http://schemas.google.com/g/2005#event."; - - // Document creation - let document = cal.xml.parseString(''); - let entry = document.documentElement; - - // Helper functions - function elemNS(ns, name) document.createElementNS(ns, name); - function addElemNS(ns, name, parent) (parent || entry).appendChild(elemNS(ns, name)); - - // Basic elements - let kindElement = addElemNS(atomNS, "category"); - kindElement.setAttribute("scheme", "http://schemas.google.com/g/2005#kind"); - kindElement.setAttribute("term", "http://schemas.google.com/g/2005#event"); - - let titleElement = addElemNS(atomNS, "title"); - titleElement.setAttribute("type", "text"); - titleElement.textContent = aItem.title; - - // atom:content - let contentElement = addElemNS(atomNS, "content"); - contentElement.setAttribute("type", "text"); - contentElement.textContent = aItem.getProperty("DESCRIPTION") || ""; - - // atom:author - let authorElement = addElemNS(atomNS, "author"); - addElemNS(atomNS, "name", authorElement).textContent = aAuthorName || aAuthorEmail; - addElemNS(atomNS, "email", authorElement).textContent = aAuthorEmail; - - // gd:transparency - let transpElement = addElemNS(gdNS, "transparency"); - let transpValue = aItem.getProperty("TRANSP") || "opaque"; - transpElement.setAttribute("value", kEVENT_SCHEMA + transpValue.toLowerCase()); - - // gd:eventStatus - let status = aItem.status || "confirmed"; - if (status == "CANCELLED") { - // If the status is canceled, then the event will be deleted. Since the - // user didn't choose to delete the event, we will protect him and not - // allow this status to be set - throw new Components.Exception("", - Components.results.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA); - } else if (status == "NONE") { - status = "CONFIRMED"; - } - addElemNS(gdNS, "eventStatus").setAttribute("value", kEVENT_SCHEMA + status.toLowerCase()); - - // gd:where - addElemNS(gdNS, "where").setAttribute("valueString", aItem.getProperty("LOCATION") || ""); - - // gd:who - if (Preferences.get("calendar.google.enableAttendees", false)) { - // XXX Only parse attendees if they are enabled, due to bug 407961 - - let attendees = aItem.getAttendees({}); - if (aItem.organizer) { - // Taking care of the organizer is the same as taking care of any other - // attendee. Add the organizer to the local attendees list. - attendees.push(aItem.organizer); - } - - const attendeeStatusMap = { - "REQ-PARTICIPANT": "required", - "OPT-PARTICIPANT": "optional", - "NON-PARTICIPANT": null, - "CHAIR": null, - - "NEEDS-ACTION": "invited", - "ACCEPTED": "accepted", - "DECLINED": "declined", - "TENTATIVE": "tentative", - "DELEGATED": "tentative" - }; - - for each (let attendee in attendees) { - if (attendee.userType && attendee.userType != "INDIVIDUAL") { - // We can only take care of individuals. - continue; - } - - let xmlAttendee = addElemNS(gdNS, "who"); - - // Strip "mailto:" part - xmlAttendee.setAttribute("email", attendee.id.replace(/^mailto:/, "")); - - if (attendee.isOrganizer) { - xmlAttendee.setAttribute("rel", kEVENT_SCHEMA + "organizer"); - } else { - xmlAttendee.setAttribute("rel", kEVENT_SCHEMA + "attendee"); - } - - if (attendee.commonName) { - xmlAttendee.setAttribute("valueString", attendee.commonName); - } - - if (attendeeStatusMap[attendee.role]) { - let attendeeTypeElement = addElemNS(gdNS, "attendeeType", xmlAttendee); - let attendeeTypeValue = kEVENT_SCHEMA + attendeeStatusMap[attendee.role]; - attendeeTypeElement.setAttribute("value", attendeeTypeValue); - } - - if (attendeeStatusMap[attendee.participationStatus]) { - let attendeeStatusElement = addElemNS(gdNS, "attendeeStatus", xmlAttendee); - let attendeeStatusValue = kEVENT_SCHEMA + attendeeStatusMap[attendee.participationStatus]; - attendeeStatusElement.setAttribute("value", attendeeStatusValue); - } - } - } - - // Don't notify attendees by default. Use a preference in case the user - // wants this to be turned on. - let notify = Preferences.get("calendar.google.sendEventNotifications", false); - addElemNS(gcalNS, "sendEventNotifications").setAttribute("value", notify ? "true" : "false"); - - // gd:when - let duration = aItem.endDate.subtractDate(aItem.startDate); - let whenElement; - if (!aItem.recurrenceInfo) { - // gd:when isn't allowed for recurring items where gd:recurrence is set - whenElement = addElemNS(gdNS, "when"); - whenElement.setAttribute("startTime", cal.toRFC3339(aItem.startDate)); - whenElement.setAttribute("endTime", cal.toRFC3339(aItem.endDate)); - } - - // gd:reminder - let alarms = aItem.getAlarms({}); - let actionMap = { - DISPLAY: "alert", - EMAIL: "email", - SMS: "sms" - }; - if (selfIsOrganizer) { - for (let i = 0; i < 5 && i < alarms.length; i++) { - let alarm = alarms[i]; - let gdReminder; - if (aItem.recurrenceInfo) { - // On recurring items, set the reminder directly in the tag. - gdReminder = addElemNS(gdNS, "reminder"); - } else { - // Otherwise, its a child of the gd:when element - gdReminder = addElemNS(gdNS, "reminder", whenElement); - } - if (alarm.related == alarm.ALARM_RELATED_ABSOLUTE) { - // Setting an absolute date can be done directly. Google will take - // care of calculating the offset. - gdReminder.setAttribute("absoluteTime", cal.toRFC3339(alarm.alarmDate)); - } else { - let alarmOffset = alarm.offset; - if (alarm.related == alarm.ALARM_RELATED_END) { - // Google always uses an alarm offset related to the start time - // for relative alarms. - alarmOffset = alarmOffset.clone(); - alarmOffset.addDuration(duration); - } - - gdReminder.setAttribute("minutes", -alarmOffset.inSeconds / 60); - gdReminder.setAttribute("method", actionMap[alarm.action] || "alert"); - } - } - } else if (alarms.length) { - // We need to reset this so the item gets returned correctly. - aItem.clearAlarms(); - } - - // gd:extendedProperty (alarmLastAck) - addExtendedProperty("X-MOZ-LASTACK", cal.toRFC3339(aItem.alarmLastAck)); - - // XXX While Google now supports multiple alarms and alarm values, we still - // need to fix bug 353492 first so we can better take care of finding out - // what alarm is used for snoozing. - - // gd:extendedProperty (snooze time) - let itemSnoozeTime = aItem.getProperty("X-MOZ-SNOOZE-TIME"); - let icalSnoozeTime = null; - if (itemSnoozeTime) { - // The propery is saved as a string, translate back to calIDateTime. - icalSnoozeTime = cal.createDateTime(); - icalSnoozeTime.icalString = itemSnoozeTime; - } - addExtendedProperty("X-MOZ-SNOOZE-TIME", cal.toRFC3339(icalSnoozeTime)); - - // gd:extendedProperty (snooze recurring alarms) - let snoozeValue = ""; - if (aItem.recurrenceInfo) { - // This is an evil workaround since we don't have a really good system - // to save the snooze time for recurring alarms or even retrieve them - // from the event. This should change when we have multiple alarms - // support. - let snoozeObj = {}; - let enumerator = aItem.propertyEnumerator; - while (enumerator.hasMoreElements()) { - let prop = enumerator.getNext().QueryInterface(Components.interfaces.nsIProperty); - if (prop.name.substr(0, 18) == "X-MOZ-SNOOZE-TIME-") { - // We have a snooze time for a recurring event, add it to our object - snoozeObj[prop.name.substr(18)] = prop.value; - } - } - snoozeValue = JSON.stringify(snoozeObj); - } - // Now save the snooze object in source format as an extended property. Do - // so always, since its currently impossible to unset extended properties. - addExtendedProperty("X-GOOGLE-SNOOZE-RECUR", snoozeValue); - - // gd:visibility - let privacy = aItem.privacy || "default"; - addElemNS(gdNS, "visibility").setAttribute("value", kEVENT_SCHEMA + privacy.toLowerCase()); - - // categories - // Google does not support categories natively, but allows us to store data - // as an "extendedProperty", so we do here - addExtendedProperty("X-MOZ-CATEGORIES", - categoriesArrayToString(aItem.getCategories({}))); - - // gd:recurrence - if (aItem.recurrenceInfo) { - try { - const kNEWLINE = "\r\n"; - let icalString; - let recurrenceItems = aItem.recurrenceInfo.getRecurrenceItems({}); - - // Dates of the master event - let startTZID = aItem.startDate.timezone.tzid; - let endTZID = aItem.endDate.timezone.tzid; - icalString = "DTSTART;TZID=" + startTZID - + ":" + aItem.startDate.icalString + kNEWLINE - + "DTEND;TZID=" + endTZID - + ":" + aItem.endDate.icalString + kNEWLINE; - - // Add all recurrence items to the ical string - for each (let ritem in recurrenceItems) { - let prop = ritem.icalProperty; - let wrappedRItem = cal.wrapInstance(wrappedRItem, Components.interfaces.calIRecurrenceDate); - if (wrappedRItem) { - // EXDATES require special casing, since they might contain - // a TZID. To avoid the need for conversion of TZID strings, - // convert to UTC before serialization. - prop.valueAsDatetime = wrappedRItem.date.getInTimezone(cal.UTC()); - } - icalString += prop.icalString; - } - - // Put the ical string in a tag - addElemNS(gdNS, "recurrence").textContent = icalString + kNEWLINE; - } catch (e) { - cal.ERROR("[calGoogleCalendar] Error: " + e); - } - } - - // gd:originalEvent - if (aItem.recurrenceId) { - let originalEvent = addElemNS(gdNS, "originalEvent"); - originalEvent.setAttribute("id", aItem.parentItem.id); - - let origWhen = addElemNS(gdNS, "when", originalEvent) - origWhen.setAttribute("startTime", cal.toRFC3339(aItem.recurrenceId.getInTimezone(cal.UTC()))); - } - - // While it may sometimes not work out, we can always try to set the uid and - // sequence properties - let sequence = aItem.getProperty("SEQUENCE"); - if (sequence) { - addElemNS(gcalNS, "sequence").setAttribute("value", sequence); - } - addElemNS(gcalNS, "uid").setAttribute("value", aItem.id || ""); - - // XXX Google currently has no priority support. See - // http://code.google.com/p/google-gdata/issues/detail?id=52 - // for details. - - return document; -} - -/** - * relevantFieldsMatch - * Tests if all google supported fields match - * - * @param a The reference item - * @param b The comparing item - * @return true if all relevant fields match, otherwise false - */ -function relevantFieldsMatch(a, b) { - - // flat values - if (a.id != b.id || - a.title != b.title || - a.status != b.status || - a.privacy != b.privacy) { - return false; - } - - function compareNotNull(prop) { - let ap = a[prop]; - let bp = b[prop]; - return (ap && !bp || !ap && bp || - (typeof(ap) == 'object' && ap && bp && - ap.compare && ap.compare(bp))); - } - - // Object flat values - if (compareNotNull("recurrenceInfo") || - compareNotNull("alarmLastAck") || - /* Compare startDate and endDate */ - compareNotNull("startDate") || - compareNotNull("endDate") || - (a.startDate.isDate != b.startDate.isDate) || - (a.endDate.isDate != b.endDate.isDate)) { - return false; - } - - // Properties - const kPROPERTIES = ["DESCRIPTION", "TRANSP", "X-GOOGLE-EDITURL", - "LOCATION", "X-MOZ-SNOOZE-TIME"]; - - for each (let p in kPROPERTIES) { - // null and an empty string should be handled as non-relevant - if ((a.getProperty(p) || "") != (b.getProperty(p) || "")) { - return false; - } - } - - // categories - let aCat = a.getCategories({}); - let bCat = b.getCategories({}); - if ((aCat.length != bCat.length) || - aCat.some(function notIn(cat) { return (bCat.indexOf(cat) == -1); })) { - return false; - } - - // attendees and organzier - let aa = a.getAttendees({}); - let ab = b.getAttendees({}); - if (aa.length != ab.length) { - return false; - } - - if ((a.organizer && !b.organizer) || - (!a.organizer && b.organizer) || - (a.organizer && b.organizer && a.organizer.id != b.organizer.id)) { - return false; - } - - // go through attendees in a, check if its id is in b - for each (let attendee in aa) { - let ba = b.getAttendeeById(attendee.id); - if (!ba || - ba.participationStatus != attendee.participationStatus || - ba.commonName != attendee.commonName || - ba.isOrganizer != attendee.isOrganizer || - ba.role != attendee.role) { - return false; - } - } - - // Alarms - aa = a.getAlarms({}); - ab = b.getAlarms({}); - - if (aa.length != ab.length) { - return false; - } - - let alarmMap = {}; - for each (let alarm in aa) { - alarmMap[alarm.icalString] = true; - } - let found = 0; - for each (let alarm in ab) { - if (alarm.icalString in alarmMap) { - found++; - } - } - - if (found != ab.length) { - return false; - } - - // Recurrence Items - if (a.recurrenceInfo) { - let ra = a.recurrenceInfo.getRecurrenceItems({}); - let rb = b.recurrenceInfo.getRecurrenceItems({}); - - // If we have more or less, it definitly changed. - if (ra.length != rb.length) { - return false; - } - - // I assume that if the recurrence pattern has not changed, the order - // of the recurrence items should not change. Anything more will be - // very expensive. - for (let i = 0; i < ra.length; i++) { - if (ra[i].icalProperty.icalString != - rb[i].icalProperty.icalString) { - return false; - } - } - } - - return true; -} - -/** - * getItemEditURI - * Helper to get the item's edit URI - * - * @param aItem The item to get it from - * @return The edit URI - */ -function getItemEditURI(aItem) { - - ASSERT(aItem); - let edituri = aItem.getProperty("X-GOOGLE-EDITURL"); - if (!edituri) { - // If the item has no edit uri, it is read-only - throw new Components.Exception("The item is readonly", Components.interfaces.calIErrors.CAL_IS_READONLY); - } - return edituri; -} - -function getIdFromEntry(aXMLEntry) { - let id = gdataXPathFirst(aXMLEntry, 'gCal:uid/@value'); - return id.replace(/@google.com/,""); -} - -function getRecurrenceIdFromEntry(aXMLEntry, aTimezone) { - let rId = gdataXPathFirst(aXMLEntry, 'gd:originalEvent/gd:when/@startTime'); - return (rId ? cal.fromRFC3339(rId.toString(), aTimezone) : null); -} - -/** - * XMLEntryToItem - * Converts a string of xml data to a calIEvent. - * - * @param aXMLEntry The xml data of the item - * @param aTimezone The timezone the event is most likely in - * @param aCalendar The calendar this item will belong to. This needs to - * be a calIGoogleCalendar instance. - * @param aReferenceItem The item to apply the information from the xml to. - * If null, a new item will be used. - * @return The calIEvent with the item data. - */ -function XMLEntryToItem(aXMLEntry, aTimezone, aCalendar, aReferenceItem) { - - function getExtendedProperty(x) gdataXPathFirst(aXMLEntry, 'gd:extendedProperty[@name="' + x + '"]/@value'); - - if (!aXMLEntry) { - throw new Components.Exception("", Components.results.NS_ERROR_FAILURE); - } else if (typeof aXMLEntry == "string") { - aXMLEntry = cal.xml.parseString(aXMLEntry); - } - - let item = (aReferenceItem ? aReferenceItem.clone() : cal.createEvent()); - - try { - // id - item.id = getIdFromEntry(aXMLEntry); - - // sequence - item.setProperty("SEQUENCE", gdataXPathFirst(aXMLEntry, 'gCal:sequence/@value') || 0); - - // link (edit url) - // Since Google doesn't set the edit url to be https if the request is - // https, we need to work around this here. - let editUrl = gdataXPathFirst(aXMLEntry, 'atom:link[@rel="edit"]/@href'); - if (aCalendar.uri.schemeIs("https")) { - editUrl = editUrl.replace(/^http:/, "https:"); - } - item.setProperty("X-GOOGLE-EDITURL", editUrl); - - // link (alternative representation, html) - let htmlUrl = gdataXPathFirst(aXMLEntry, 'atom:link[@rel="alternate"]/@href'); - if (aCalendar.uri.schemeIs("https")) { - htmlUrl = htmlUrl.replace(/^http:/, "https:"); - } - item.setProperty("URL", htmlUrl); - - // title - item.title = gdataXPathFirst(aXMLEntry, 'atom:title[@type="text"]/text()'); - - // content - item.setProperty("DESCRIPTION", gdataXPathFirst(aXMLEntry, 'atom:content[@type="text"]/text()')); - - // gd:transparency - item.setProperty("TRANSP", gdataXPathFirst(aXMLEntry, 'gd:transparency/@value').substring(39).toUpperCase()); - - // gd:eventStatus - item.status = gdataXPathFirst(aXMLEntry, 'gd:eventStatus/@value').substring(39).toUpperCase(); - - // gd:reminder (preparation) - // If a reference item was passed, it may already contain alarms. Since - // we have no alarm id or such and the alarms are contained in every - // feed, we can go ahead and clear the alarms here. - item.clearAlarms(); - - /** - * Helper function to parse all reminders in a tagset. - * - * @param reminderTags The tagset to parse. - */ - function parseReminders(reminderTags) { - let organizerEmail = gdataXPathFirst(aXMLEntry, 'gd:who[@rel="http://schemas.google.com/g/2005#event.organizer"]/@email'); - if (organizerEmail != aCalendar.googleCalendarName) { - // We are not the organizer, so its not smart to set alarms on - // this event. - return; - } - const actionMap = { - email: "EMAIL", - alert: "DISPLAY", - sms: "SMS" - }; - for each (let reminderTag in reminderTags) { - let alarm = cal.createAlarm(); - alarm.action = actionMap[reminderTag.getAttribute("method")] || "DISPLAY"; - - let absoluteTime = reminderTag.getAttribute("absoluteTime"); - if (absoluteTime) { - alarm.related = Components.interfaces.calIAlarm.ALARM_RELATED_ABSOLUTE; - alarm.alarmDate = cal.fromRFC3339(absoluteTime, aTimezone); - } else { - alarm.related = Components.interfaces.calIAlarm.ALARM_RELATED_START; - let alarmOffset = cal.createDuration(); - let days = reminderTag.getAttribute("days"); - let hours = reminderTag.getAttribute("hours"); - let minutes = reminderTag.getAttribute("minutes"); - - if (days) { - alarmOffset.days = -days; - } else if (hours) { - alarmOffset.hours = -hours; - } else if (minutes) { - alarmOffset.minutes = -minutes; - } else { - // Invalid alarm, skip it - continue; - } - alarmOffset.normalize(); - alarm.offset = alarmOffset; - } - item.addAlarm(alarm); - } - } - - // gd:when - let recurrenceInfo = gdataXPathFirst(aXMLEntry, 'gd:recurrence/text()'); - if (!recurrenceInfo || recurrenceInfo.length == 0) { - // If no recurrence information is given, then there will only be - // one gd:when tag. Otherwise, we will be parsing the startDate from - // the recurrence information. - item.startDate = cal.fromRFC3339(gdataXPathFirst(aXMLEntry, 'gd:when/@startTime'), aTimezone); - item.endDate = cal.fromRFC3339(gdataXPathFirst(aXMLEntry, 'gd:when/@endTime'), aTimezone); - - if (!item.endDate) { - // We have a zero-duration event - item.endDate = item.startDate.clone(); - } - - // gd:reminder - parseReminders(gdataXPath(aXMLEntry, 'gd:when/gd:reminder')); - } else { - if (!item.recurrenceInfo) { - item.recurrenceInfo = cal.createRecurrenceInfo(item); - } else { - item.recurrenceInfo.clearRecurrenceItems(); - } - - // We don't really care about google's timezone info for - // now. This may change when bug 314339 is fixed. Split out - // the timezone information so we only have the first bit - let vevent = recurrenceInfo; - let splitpos = recurrenceInfo.indexOf("BEGIN:VTIMEZONE"); - if (splitpos > -1) { - // Sometimes (i.e if only DATE values are specified), no - // timezone info is contained. Only remove it if it shows up. - vevent = recurrenceInfo.substring(0, splitpos); - } - - vevent = "BEGIN:VEVENT\n" + vevent + "END:VEVENT"; - let icsService = cal.getIcsService(); - - let rootComp = icsService.parseICS(vevent, gdataTimezoneService); - let i = 0; - let hasRecurringRules = false; - for (let prop in cal.ical.propertyIterator(rootComp)) { - switch (prop.propertyName) { - case "EXDATE": - let recItem = Components.classes["@mozilla.org/calendar/recurrence-date;1"] - .createInstance(Components.interfaces.calIRecurrenceDate); - try { - recItem.icalProperty = prop; - item.recurrenceInfo.appendRecurrenceItem(recItem); - hasRecurringRules = true; - } catch (e) { - Components.utils.reportError(e); - } - break; - case "RRULE": - let recRule = cal.createRecurrenceRule(); - try { - recRule.icalProperty = prop; - item.recurrenceInfo.appendRecurrenceItem(recRule); - hasRecurringRules = true; - } catch (e) { - Components.utils.reportError(e); - } - break; - case "DTSTART": - item.startDate = prop.valueAsDatetime; - break; - case "DTEND": - item.endDate = prop.valueAsDatetime; - break; - } - } - - if (!hasRecurringRules) { - // Sometimes Google gives us events that have - // but contain no recurrence rules. Treat the event as a normal - // event. See gdata issue 353. - item.recurrenceInfo = null; - } - - // gd:reminder (for recurring events) - // This element is supplied as a direct child to the element - // for recurring items. - parseReminders(gdataXPath(aXMLEntry, 'gd:reminder')); - } - - // gd:recurrenceException - let exceptions = gdataXPath(aXMLEntry, 'gd:recurrenceException[@specialized="true"]/gd:entryLink/atom:entry'); - for each (let exception in exceptions) { - // We only want specialized exceptions, mainly becuase I haven't - // quite found out if a non-specialized exception also corresponds - // to a normal exception as libical knows it. - let excItem = XMLEntryToItem(exception, aTimezone, aCalendar); - - // Google uses the status field to reflect negative exceptions. - if (excItem.status == "CANCELED") { - item.recurrenceInfo.removeOccurrenceAt(excItem.recurrenceId); - } else { - excItem.calendar = aCalendar.superCalendar; - item.recurrenceInfo.modifyException(excItem, true); - } - } - - // gd:extendedProperty (alarmLastAck) - item.alarmLastAck = cal.fromRFC3339(getExtendedProperty("X-MOZ-LASTACK"), aTimezone); - - // gd:extendedProperty (snooze time) - let dtSnoozeTime = cal.fromRFC3339(getExtendedProperty("X-MOZ-SNOOZE-TIME"), aTimezone); - let snoozeProperty = (dtSnoozeTime ? dtSnoozeTime.icalString : null); - item.setProperty("X-MOZ-SNOOZE-TIME", snoozeProperty); - - // gd:extendedProperty (snooze recurring alarms) - if (item.recurrenceInfo) { - // Transform back the string into our snooze properties - let snoozeObj; - try { - let snoozeString = getExtendedProperty("X-GOOGLE-SNOOZE-RECUR"); - snoozeObj = JSON.parse(snoozeString); - } catch (e) { - // Just swallow parsing errors, not so important. - } - - if (snoozeObj) { - for (let rid in snoozeObj) { - item.setProperty("X-MOZ-SNOOZE-TIME-" + rid, - snoozeObj[rid]); - } - } - } - - // gd:where - item.setProperty("LOCATION", gdataXPathFirst(aXMLEntry, 'gd:where/@valueString')); - - // gd:who - if (Preferences.get("calendar.google.enableAttendees", false)) { - // XXX Only parse attendees if they are enabled, due to bug 407961 - - // This object can easily translate Google's values to our values. - const attendeeStatusMap = { - // role - "event.optional": "OPT-PARTICIPANT", - "event.required": "REQ-PARTICIPANT", - - // Participation Statii - "event.accepted": "ACCEPTED", - "event.declined": "DECLINED", - "event.invited": "NEEDS-ACTION", - "event.tentative": "TENTATIVE" - }; - - // Clear all attendees in case a reference item was passed - item.removeAllAttendees(); - - // Iterate all attendee tags. - for each (let who in gdataXPath(aXMLEntry, 'gd:who')) { - let attendee = cal.createAttendee(); - let rel = who.getAttribute("rel").substring(33); - let type = gdataXPathFirst(who, 'gd:attendeeType/@value').substring(33); - let status = gdataXPathFirst(who, 'gd:attendeeStatus/@value').substring(33); - - attendee.id = "mailto:" + who.getAttribute("email") - attendee.commonName = who.getAttribute("valueString"); - attendee.rsvp = "FALSE"; - attendee.userType = "INDIVIDUAL"; - attendee.isOrganizer = (rel == "event.organizer"); - attendee.participationStatus = attendeeStatusMap[status]; - attendee.role = attendeeStatusMap[type] - attendee.makeImmutable(); - - if (attendee.isOrganizer) { - item.organizer = attendee; - } else { - item.addAttendee(attendee); - } - } - } - - // gd:originalEvent - item.recurrenceId = getRecurrenceIdFromEntry(aXMLEntry, aTimezone); - - // gd:visibility - item.privacy = gdataXPathFirst(aXMLEntry, "gd:visibility/@value").substring(39).toUpperCase(); - - // category - // Google does not support categories natively, but allows us to store - // data as an "extendedProperty", and here it's going to be retrieved - // again - let categories = cal.categoriesStringToArray(getExtendedProperty("X-MOZ-CATEGORIES")); - item.setCategories(categories.length, categories); - - // published - let createdText = gdataXPathFirst(aXMLEntry, 'atom:published/text()'); - item.setProperty("CREATED", cal.fromRFC3339(createdText, aTimezone).getInTimezone(cal.UTC())); - - // updated (This must be set last!) - let lastmodText = gdataXPathFirst(aXMLEntry, 'atom:updated/text()'); - item.setProperty("LAST-MODIFIED", cal.fromRFC3339(lastmodText, aTimezone).getInTimezone(cal.UTC())); - - // XXX Google currently has no priority support. See - // http://code.google.com/p/google-gdata/issues/detail?id=52 - // for details. - } catch (e) { - cal.ERROR("Error parsing XML stream" + e); - throw e; - } - return item; -} - -/** - * Expand an item to occurrences, if the operation's item filter requests it. - * Otherwise returns the item in an array. - * - * @param aItem The item to expand - * @param aOperation The calIGoogleRequest that contains the filter and - * ranges. - * @return The (possibly expanded) items in an array. - */ -function expandItems(aItem, aOperation) { - let expandedItems; - if (aOperation.itemFilter & - Components.interfaces.calICalendar.ITEM_FILTER_CLASS_OCCURRENCES) { - expandedItems = aItem.getOccurrencesBetween(aOperation.itemRangeStart, - aOperation.itemRangeEnd, - {}); - cal.LOG("[calGoogleCalendar] Expanded item " + aItem.title + " to " + - expandedItems.length + " items"); - } - return expandedItems || [aItem]; -} - -/** - * Returns true if the exception passed is one that should cause the cache - * layer to retry the operation. This is usually a network error or other - * temporary error - * - * @param e The exception to check - */ -function isCacheException(e) { - // Stolen from nserror.h - const NS_ERROR_MODULE_NETWORK = 6; - function NS_ERROR_GET_MODULE(code) { - return (((code >> 16) - 0x45) & 0x1fff); - } - - if (NS_ERROR_GET_MODULE(e.result) == NS_ERROR_MODULE_NETWORK && - !Components.isSuccessCode(e.result)) { - // This is a network error, which most likely means we should - // retry it some time. - return true; - } - - // Other potential errors we want to retry with - switch (e.result) { - case Components.results.NS_ERROR_NOT_AVAILABLE: - return true; - default: - return false; - } -} - -/** - * Helper prototype to set a certain variable to the first item passed via get - * listener. Cleans up code. - */ -function syncSetter(aObj) { - this.mObj = aObj -} -syncSetter.prototype = { - - onGetResult: function syncSetter_onGetResult(aCal, - aStatus, - aIID, - aDetail, - aCount, - aItems) { - this.mObj.value = aItems[0]; - }, - - onOperationComplete: function syncSetter_onOperationComplete(aCal, - aStatus, - aOpType, - aId, - aDetail) { - - if (!Components.isSuccessCode(aStatus)) { - this.mObj.value = null; - } - } -}; - -/** - * Helper function to create a timer in a context where window.setTimeout is not - * available - * - * @param aFunc The function to call when the timer fires - * @param aTimeout The timeout in milliseconds. - * @param aThis (optional) The |this| object to call the function with. - */ -function setTimeout(aFunc, aTimeout, aThis) { - let timerCallback = { - notify: function setTimeout_notify() { - aFunc.call(aThis); - } - }; - let timer = Components.classes["@mozilla.org/timer;1"] - .createInstance(Components.interfaces.nsITimer); - timer.initWithCallback(timerCallback, aTimeout, timer.TYPE_ONE_SHOT); -} - -/** - * LOGitem - * Custom logging functions - */ -function LOGitem(item) { - if (!item) { - return; - } - - let attendees = item.getAttendees({}); - let attendeeString = ""; - for each (let a in attendees) { - attendeeString += "\n" + LOGattendee(a); - } - - let rstr = "\n"; - if (item.recurrenceInfo) { - let ritems = item.recurrenceInfo.getRecurrenceItems({}); - for each (let ritem in ritems) { - rstr += "\t\t" + ritem.icalProperty.icalString; - } - - rstr += "\tExceptions:\n"; - let exids = item.recurrenceInfo.getExceptionIds({}); - for each (let exc in exids) { - rstr += "\t\t" + exc + "\n"; - } - } - - let astr = "\n"; - let alarms = item.getAlarms({}); - for each (let alarm in alarms) { - astr += "\t\t" + LOGalarm(alarm) + "\n"; - } - - cal.LOG("[calGoogleCalendar] Logging calIEvent:" + - "\n\tid:" + item.id + - "\n\tediturl:" + item.getProperty("X-GOOGLE-EDITURL") + - "\n\tcreated:" + item.getProperty("CREATED") + - "\n\tupdated:" + item.getProperty("LAST-MODIFIED") + - "\n\ttitle:" + item.title + - "\n\tcontent:" + item.getProperty("DESCRIPTION") + - "\n\ttransparency:" + item.getProperty("TRANSP") + - "\n\tstatus:" + item.status + - "\n\tstartTime:" + item.startDate.toString() + - "\n\tendTime:" + item.endDate.toString() + - "\n\tlocation:" + item.getProperty("LOCATION") + - "\n\tprivacy:" + item.privacy + - "\n\tsequence:" + item.getProperty("SEQUENCE") + - "\n\talarmLastAck:" + item.alarmLastAck + - "\n\tsnoozeTime:" + item.getProperty("X-MOZ-SNOOZE-TIME") + - "\n\tisOccurrence: " + (item.recurrenceId != null) + - "\n\tOrganizer: " + LOGattendee(item.organizer) + - "\n\tAttendees: " + attendeeString + - "\n\trecurrence: " + (rstr.length > 1 ? "yes: " + rstr : "no") + - "\n\talarms: " + (astr.length > 1 ? "yes: " + astr : "no")); -} - -function LOGattendee(aAttendee, asString) { - return aAttendee && - ("\n\t\tID: " + aAttendee.id + - "\n\t\t\tName: " + aAttendee.commonName + - "\n\t\t\tRsvp: " + aAttendee.rsvp + - "\n\t\t\tIs Organizer: " + (aAttendee.isOrganizer ? "yes" : "no") + - "\n\t\t\tRole: " + aAttendee.role + - "\n\t\t\tStatus: " + aAttendee.participationStatus); -} - -function LOGalarm(aAlarm) { - if (!aAlarm) { - return ""; - } - - let enumerator = aAlarm.propertyEnumerator; - let xpropstr = ""; - while (enumerator.hasMoreElements()) { - let el = enumerator.getNext(); - xpropstr += "\n\t\t\t" + el.key + ":" + el.value; - } - - return ("\n\t\tAction: " + aAlarm.action + - "\n\t\tOffset: " + (aAlarm.offset && aAlarm.offset.toString()) + - "\n\t\talarmDate: " + (aAlarm.alarmDate && aAlarm.alarmDate.toString()) + - "\n\t\trelated: " + aAlarm.related + - "\n\t\trepeat: " + aAlarm.repeat + - "\n\t\trepeatOffset: " + (aAlarm.repeatOffset && aAlarm.repeatOffset.toString()) + - "\n\t\trepeatDate: " + (aAlarm.repeatDate && aAlarm.repeatDate.toString()) + - "\n\t\tdescription: " + aAlarm.description + - "\n\t\tsummary: " + aAlarm.summary + - "\n\t\tproperties: " + (xpropstr.length > 0 ? "yes:" + xpropstr : "no")); -} - -function LOGinterval(aInterval) { - const fbtypes = Components.interfaces.calIFreeBusyInterval; - if (aInterval.freeBusyType == fbtypes.FREE) { - type = "FREE"; - } else if (aInterval.freeBusyType == fbtypes.BUSY) { - type = "BUSY"; - } else { - type = aInterval.freeBusyType + "(UNKNOWN)"; - } - - cal.LOG("[calGoogleCalendar] Interval from " + - aInterval.interval.start + " to " + aInterval.interval.end + - " is " + type); -} diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/Makefile.in thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/Makefile.in --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/Makefile.in 2014-12-08 19:30:40.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/Makefile.in 1970-01-01 00:00:00.000000000 +0000 @@ -1,24 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -DEPTH = @DEPTH@ -topsrcdir = @top_srcdir@ -srcdir = @srcdir@ -VPATH = @srcdir@ - -include $(DEPTH)/config/autoconf.mk - -EXTRA_SCRIPTS = \ - calGoogleCalendar.js \ - calGoogleRequest.js \ - calGoogleSession.js \ - calGoogleUtils.js \ - $(NULL) - -# Use NSINSTALL to make the directory, as there's no mtime to preserve. -libs:: $(EXTRA_SCRIPTS) - if test ! -d $(FINAL_TARGET)/js; then $(NSINSTALL) -D $(FINAL_TARGET)/js; fi - $(INSTALL) $^ $(FINAL_TARGET)/js - -include $(topsrcdir)/config/rules.mk diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/moz.build thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/moz.build --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/components/moz.build 2014-12-08 19:30:40.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/components/moz.build 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -EXTRA_COMPONENTS += [ - 'calGoogleCalendarModule.js', - 'calGoogleCalendarModule.manifest', -] - diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/content/browserRequest.css thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/content/browserRequest.css --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/content/browserRequest.css 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/content/browserRequest.css 2014-12-15 12:28:32.000000000 +0000 @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#security-button { + width: 20px; + margin-top: -1px; + margin-right: 5px; + background-repeat: no-repeat; +} + +#security-button[level="high"], +#security-button[level="low"] { + background-image: url("chrome://messenger/skin/icons/secure.png"); +} + +#security-button[level="broken"] { + background-image: url("chrome://messenger/skin/icons/insecure.png"); +} + +#security-button[loading="true"] { + background-image: url("chrome://messenger/skin/icons/loading.png"); + background-position: 4px 3px; +} + +@media (min-resolution: 2ddpx) { + #security-button[loading="true"] { + background-image: url("chrome://messenger/skin/icons/loading@2x.png"); + } +} + +/* +#header { + overflow: hidden; + padding: 5px; + border-bottom: 1px solid black; + font-weight: bold; + font-size: 1.2em; +} +*/ + +#header { + border-bottom: 1px solid rgb(105, 105, 105); + overflow: hidden; +} + +#addressbox { + font-weight: bold; + font-size: normal; + -moz-appearance: textfield; + overflow: hidden; + margin: 5px 5px; + font-weight: normal; +} + +#headerMessage { + margin-top: 3px; + margin-bottom: 3px; +} diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/content/browserRequest.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/content/browserRequest.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/content/browserRequest.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/content/browserRequest.js 2014-12-15 12:28:32.000000000 +0000 @@ -0,0 +1,113 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const wpl = Components.interfaces.nsIWebProgressListener; + +var reporterListener = { + _isBusy: false, + get securityButton() { + delete this.securityButton; + return this.securityButton = document.getElementById("security-button"); + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Components.interfaces.nsIWebProgressListener) || + aIID.equals(Components.interfaces.nsISupportsWeakReference) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + }, + + onStateChange: function(/*in nsIWebProgress*/ aWebProgress, + /*in nsIRequest*/ aRequest, + /*in unsigned long*/ aStateFlags, + /*in nsresult*/ aStatus) { + }, + + onProgressChange: function(/*in nsIWebProgress*/ aWebProgress, + /*in nsIRequest*/ aRequest, + /*in long*/ aCurSelfProgress, + /*in long */aMaxSelfProgress, + /*in long */aCurTotalProgress, + /*in long */aMaxTotalProgress) { + }, + + onLocationChange: function(/*in nsIWebProgress*/ aWebProgress, + /*in nsIRequest*/ aRequest, + /*in nsIURI*/ aLocation) { + document.getElementById("headerMessage").textContent = aLocation.spec; + }, + + onStatusChange: function(/*in nsIWebProgress*/ aWebProgress, + /*in nsIRequest*/ aRequest, + /*in nsresult*/ aStatus, + /*in wstring*/ aMessage) { + }, + + onSecurityChange: function(/*in nsIWebProgress*/ aWebProgress, + /*in nsIRequest*/ aRequest, + /*in unsigned long*/ aState) { + const wpl_security_bits = wpl.STATE_IS_SECURE | + wpl.STATE_IS_BROKEN | + wpl.STATE_IS_INSECURE | + wpl.STATE_SECURE_HIGH | + wpl.STATE_SECURE_MED | + wpl.STATE_SECURE_LOW; + var browser = document.getElementById("requestFrame"); + var level; + + switch (aState & wpl_security_bits) { + case wpl.STATE_IS_SECURE | wpl.STATE_SECURE_HIGH: + level = "high"; + break; + case wpl.STATE_IS_SECURE | wpl.STATE_SECURE_MED: + case wpl.STATE_IS_SECURE | wpl.STATE_SECURE_LOW: + level = "low"; + break; + case wpl.STATE_IS_BROKEN: + level = "broken"; + break; + } + if (level) { + this.securityButton.setAttribute("level", level); + this.securityButton.hidden = false; + } else { + this.securityButton.hidden = true; + this.securityButton.removeAttribute("level"); + } + this.securityButton.setAttribute("tooltiptext", + browser.securityUI.tooltipText); + } +} + +function cancelRequest() +{ + reportUserClosed(); + window.close(); +} + +function reportUserClosed() +{ + let request = window.arguments[0].wrappedJSObject; + request.cancelled(); +} + +function loadRequestedUrl() +{ + let request = window.arguments[0].wrappedJSObject; + document.getElementById("headerMessage").textContent = request.promptText; + let account = request.account; + if (request.iconURI != "") + document.getElementById("headerImage").src = request.iconURI; + + var browser = document.getElementById("requestFrame"); + browser.addProgressListener(reporterListener, + Components.interfaces.nsIWebProgress.NOTIFY_ALL); + var url = request.url; + if (url != "") { + browser.setAttribute("src", url); + document.getElementById("headerMessage").textContent = url; + } + request.loaded(window, browser.webProgress); +} diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/content/browserRequest.xul thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/content/browserRequest.xul --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/calendar/providers/gdata/content/browserRequest.xul 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/calendar/providers/gdata/content/browserRequest.xul 2014-12-15 12:28:32.000000000 +0000 @@ -0,0 +1,34 @@ + + + + + + + + + + - - - @@ -196,12 +177,6 @@ { role: ROLE_CHROME_WINDOW }, - { - role: ROLE_CHROME_WINDOW - }, - { - role: ROLE_CHROME_WINDOW - }, { role: ROLE_CHROME_WINDOW } diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/accessible/tests/mochitest/tree/test_dochierarchy.html thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/accessible/tests/mochitest/tree/test_dochierarchy.html --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/accessible/tests/mochitest/tree/test_dochierarchy.html 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/accessible/tests/mochitest/tree/test_dochierarchy.html 2014-12-15 12:28:53.000000000 +0000 @@ -31,10 +31,15 @@ is(root.parentDocument, null, "Wrong parent document of root accessible"); - is(root.childDocumentCount, 1, + ok(root.childDocumentCount >= 1, "Wrong child document count of root accessible"); - is(root.getChildDocumentAt(0), tabDoc, - "Wrong child document at index 0 of root accessible"); + + var tabDocumentFound = false; + for (var i = 0; i < root.childDocumentCount && !tabDocumentFound; i++) { + tabDocumentFound = root.getChildDocumentAt(i) == tabDoc; + } + ok(tabDocumentFound, + "Tab document not found in children of the root accessible"); is(tabDoc.parentDocument, root, "Wrong parent document of tab document"); diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/accessible/tests/mochitest/tree/test_tabbrowser.xul thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/accessible/tests/mochitest/tree/test_tabbrowser.xul --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/accessible/tests/mochitest/tree/test_tabbrowser.xul 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/accessible/tests/mochitest/tree/test_tabbrowser.xul 2014-12-15 12:28:53.000000000 +0000 @@ -169,6 +169,23 @@ ] } ] + }, + { + // notificationbox + role: ROLE_PROPERTYPAGE, + children: [ + { + // browser + role: ROLE_INTERNAL_FRAME, + children: [ + { + // #document ("about:newtab" preloaded) + role: ROLE_APPLICATION + // children: [ ... ] // Ignore document content. + } + ] + } + ] } ] }; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/accessible/xpcom/AccEventGen.py thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/accessible/xpcom/AccEventGen.py --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/accessible/xpcom/AccEventGen.py 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/accessible/xpcom/AccEventGen.py 2014-12-15 12:28:53.000000000 +0000 @@ -4,7 +4,10 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -import sys, os, xpidl, makeutils +import sys, os, xpidl + +from mozbuild.makeutil import write_dep_makefile +from mozbuild.util import FileAvoidWrite def findIDL(includePath, interfaceFileName): for d in includePath: @@ -20,11 +23,9 @@ def loadEventIDL(parser, includePath, eventname): eventidl = ("nsIAccessible%s.idl" % eventname) idlFile = findIDL(includePath, eventidl) - if not idlFile in makeutils.dependencies: - makeutils.dependencies.append(idlFile) idl = p.parse(open(idlFile).read(), idlFile) idl.resolve(includePath, p) - return idl + return idl, idlFile class Configuration: def __init__(self, filename): @@ -42,6 +43,8 @@ return ("%s a%s" % (a.realtype.nativeType('in'), firstCap(a.name))) def print_header_file(fd, conf): + idl_paths = set() + fd.write("/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n") fd.write("#ifndef _mozilla_a11y_generated_AccEvents_h_\n" "#define _mozilla_a11y_generated_AccEvents_h_\n\n") @@ -52,7 +55,8 @@ for e in conf.simple_events: fd.write("#include \"nsIAccessible%s.h\"\n" % e) for e in conf.simple_events: - idl = loadEventIDL(p, options.incdirs, e) + idl, idl_path = loadEventIDL(p, options.incdirs, e) + idl_paths.add(idl_path) for iface in filter(lambda p: p.kind == "interface", idl.productions): classname = ("xpcAcc%s" % e) baseinterfaces = interfaces(iface) @@ -83,6 +87,8 @@ fd.write("#endif\n") + return idl_paths + def interfaceAttributeTypes(idl): ifaces = filter(lambda p: p.kind == "interface", idl.productions) attributes = [] @@ -98,6 +104,7 @@ write_cpp(eventname, p, fd) def print_cpp_file(fd, conf): + idl_paths = set() fd.write("/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n\n") fd.write('#include "xpcAccEvents.h"\n') @@ -108,7 +115,8 @@ types = [] for e in conf.simple_events: - idl = loadEventIDL(p, options.incdirs, e) + idl, idl_path = loadEventIDL(p, options.incdirs, e) + idl_paths.add(idl_path) types.extend(interfaceAttributeTypes(idl)) for c in types: @@ -116,7 +124,11 @@ fd.write("\n") for e in conf.simple_events: - print_cpp(loadEventIDL(p, options.incdirs, e), fd, conf, e) + idl, idl_path = loadEventIDL(p, options.incdirs, e) + idl_paths.add(idl_path) + print_cpp(idl, fd, conf, e) + + return idl_paths def attributeVariableTypeAndName(a): if a.realtype.nativeType('in').endswith('*'): @@ -194,47 +206,33 @@ def main(): - from optparse import OptionParser - o = OptionParser(usage="usage: %prog [options] configfile") - o.add_option('-I', action='append', dest='incdirs', default=['.'], + from argparse import ArgumentParser + o = ArgumentParser() + o.add_argument('-I', action='append', dest='incdirs', default=['.'], help="Directory to search for imported files") - o.add_option('-o', "--stub-output", - type='string', dest='stub_output', default=None, - help="C++ source output file", metavar="FILE") - o.add_option('--header-output', type='string', default=None, - help="Quick stub header output file", metavar="FILE") - o.add_option('--makedepend-output', type='string', default=None, - help="gnumake dependencies output file", metavar="FILE") - o.add_option('--cachedir', dest='cachedir', default=None, - help="Directory in which to cache lex/parse tables.") + o.add_argument('config', + help='Config file to load') + o.add_argument('header_output', metavar='FILE', + help="Quick stub header output file") + o.add_argument('stub_output', metavar='FILE', + help="C++ source output file") + o.add_argument('makedepend_output', metavar='FILE', + help="gnumake dependencies output file") global options - (options, filenames) = o.parse_args() - if len(filenames) != 1: - o.error("Exactly one config filename is needed.") - filename = filenames[0] - - if options.cachedir is not None: - if not os.path.isdir(options.cachedir): - os.mkdir(options.cachedir) - sys.path.append(options.cachedir) + options = o.parse_args() # Instantiate the parser. global p - p = xpidl.IDLParser(outputdir=options.cachedir) + p = xpidl.IDLParser() - conf = readConfigFile(filename) + conf = readConfigFile(options.config) - if options.stub_output is not None: - makeutils.targets.append(options.stub_output) - outfd = open(options.stub_output, 'w') - print_cpp_file(outfd, conf) - outfd.close() - if options.makedepend_output is not None: - makeutils.writeMakeDependOutput(options.makedepend_output) - if options.header_output is not None: - outfd = open(options.header_output, 'w') - print_header_file(outfd, conf) - outfd.close() + with FileAvoidWrite(options.header_output) as fh: + idl_paths = print_header_file(fh, conf) + with FileAvoidWrite(options.stub_output) as fh: + idl_paths |= print_cpp_file(fh, conf) + with FileAvoidWrite(options.makedepend_output) as fh: + write_dep_makefile(fh, options.stub_output, idl_paths) if __name__ == '__main__': main() diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/accessible/xpcom/Makefile.in thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/accessible/xpcom/Makefile.in --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/accessible/xpcom/Makefile.in 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/accessible/xpcom/Makefile.in 2014-12-15 12:28:53.000000000 +0000 @@ -19,10 +19,10 @@ -I$(LIBXUL_DIST)/sdk/bin \ $(srcdir)/AccEventGen.py \ -I $(DEPTH)/dist/idl \ - --header-output xpcAccEvents.h \ - --stub-output xpcAccEvents.cpp \ - --makedepend-output $(MDDEPDIR)/xpcAccEvents.pp \ - $(srcdir)/AccEvents.conf + $(srcdir)/AccEvents.conf \ + xpcAccEvents.h \ + xpcAccEvents.cpp \ + $(MDDEPDIR)/xpcAccEvents.pp xpcAccEvents.h: xpcAccEvents.cpp diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/accessible/xul/moz.build thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/accessible/xul/moz.build --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/accessible/xul/moz.build 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/accessible/xul/moz.build 2014-12-15 12:28:53.000000000 +0000 @@ -48,3 +48,5 @@ ] FINAL_LIBRARY = 'xul' + +FAIL_ON_WARNINGS = True diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/app/b2g.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/app/b2g.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/app/b2g.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/app/b2g.js 2014-12-15 12:28:54.000000000 +0000 @@ -677,11 +677,7 @@ pref("javascript.options.mem.high_water_mark", 6); pref("javascript.options.mem.gc_allocation_threshold_mb", 1); pref("javascript.options.mem.gc_decommit_threshold_mb", 1); -#ifdef JSGC_GENERATIONAL pref("javascript.options.mem.gc_min_empty_chunk_count", 1); -#else -pref("javascript.options.mem.gc_min_empty_chunk_count", 0); -#endif pref("javascript.options.mem.gc_max_empty_chunk_count", 2); // Show/Hide scrollbars when active/inactive diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/chrome/content/desktop.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/chrome/content/desktop.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/chrome/content/desktop.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/chrome/content/desktop.js 2014-12-15 12:28:54.000000000 +0000 @@ -56,8 +56,8 @@ function checkDebuggerPort() { // XXX: To be removed once bug 942756 lands. // We are hacking 'unix-domain-socket' pref by setting a tcp port (number). - // DebuggerServer.openListener detects that it isn't a file path (string), - // and starts listening on the tcp port given here as command line argument. + // SocketListener.open detects that it isn't a file path (string), and starts + // listening on the tcp port given here as command line argument. // Get the command line arguments that were passed to the b2g client let args; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/chrome/content/devtools/debugger.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/chrome/content/devtools/debugger.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/chrome/content/devtools/debugger.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/chrome/content/devtools/debugger.js 2014-12-15 12:28:54.000000000 +0000 @@ -17,10 +17,6 @@ return devtools; }); -XPCOMUtils.defineLazyGetter(this, "discovery", function() { - return devtools.require("devtools/toolkit/discovery/discovery"); -}); - XPCOMUtils.defineLazyGetter(this, "B2GTabList", function() { const { B2GTabList } = devtools.require("resource://gre/modules/DebuggerActors.js"); @@ -145,8 +141,10 @@ try { debug("Starting USB debugger on " + portOrPath); - this._listener = DebuggerServer.openListener(portOrPath); + this._listener = DebuggerServer.createListener(); + this._listener.portOrPath = portOrPath; this._listener.allowConnection = RemoteDebugger.prompt; + this._listener.open(); // Temporary event, until bug 942756 lands and offers a way to know // when the server is up and running. Services.obs.notifyObservers(null, "debugger-server-started", null); @@ -181,11 +179,14 @@ try { debug("Starting WiFi debugger"); - this._listener = DebuggerServer.openListener(-1); + this._listener = DebuggerServer.createListener(); + this._listener.portOrPath = -1 /* any available port */; this._listener.allowConnection = RemoteDebugger.prompt; + this._listener.discoverable = true; + this._listener.encryption = true; + this._listener.open(); let port = this._listener.port; debug("Started WiFi debugger on " + port); - discovery.addService("devtools", { port: port }); } catch (e) { debug("Unable to start WiFi debugger server: " + e); } @@ -197,7 +198,6 @@ } try { - discovery.removeService("devtools"); this._listener.close(); this._listener = null; } catch (e) { diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/chrome/content/shell.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/chrome/content/shell.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/chrome/content/shell.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/chrome/content/shell.js 2014-12-15 12:28:54.000000000 +0000 @@ -725,6 +725,8 @@ CaptivePortalLoginHelper.handleEvent(detail); break; case 'inputmethod-update-layouts': + case 'inputregistry-add': + case 'inputregistry-remove': KeyboardHelper.handleEvent(detail); break; case 'do-command': @@ -868,7 +870,17 @@ let KeyboardHelper = { handleEvent: function keyboard_handleEvent(detail) { - Keyboard.setLayouts(detail.layouts); + switch (detail.type) { + case 'inputmethod-update-layouts': + Keyboard.setLayouts(detail.layouts); + + break; + case 'inputregistry-add': + case 'inputregistry-remove': + Keyboard.inputRegistryGlue.returnMessage(detail); + + break; + } } }; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/components/ProcessGlobal.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/components/ProcessGlobal.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/components/ProcessGlobal.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/components/ProcessGlobal.js 2014-12-15 12:28:54.000000000 +0000 @@ -22,6 +22,10 @@ Cu.import('resource://gre/modules/Services.jsm'); Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +XPCOMUtils.defineLazyServiceGetter(this, "settings", + "@mozilla.org/settingsService;1", + "nsISettingsService"); + function debug(msg) { log(msg); } @@ -89,14 +93,19 @@ } }, - processWipeFile: function(text) { - log("processWipeFile " + text); + processCommandsFile: function(text) { + log("processCommandsFile " + text); let lines = text.split("\n"); lines.forEach((line) => { log(line); let params = line.split(" "); if (params[0] == "wipe") { this.wipeDir(params[1]); + } else if (params[0] == "root") { + log("unrestrict devtools"); + Services.prefs.setBoolPref("devtools.debugger.forbid-certified-apps", false); + let lock = settings.createLock(); + lock.set("developer.menu.enabled", true, null); } }); }, @@ -113,7 +122,7 @@ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); file.initWithPath(postResetFile); if (!file.exists()) { - debug("Nothing to wipe.") + debug("No additional command.") return; } @@ -122,7 +131,7 @@ (array) => { file.remove(false); let decoder = new TextDecoder(); - this.processWipeFile(decoder.decode(array)); + this.processCommandsFile(decoder.decode(array)); }, function onError(error) { debug("Error: " + error); diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/components/RecoveryService.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/components/RecoveryService.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/components/RecoveryService.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/components/RecoveryService.js 2014-12-15 12:28:54.000000000 +0000 @@ -81,19 +81,24 @@ } log("factoryReset " + reason); + let commands = []; if (reason == "wipe") { let volumeService = Cc["@mozilla.org/telephony/volume-service;1"] .getService(Ci.nsIVolumeService); let volNames = volumeService.getVolumeNames(); log("Found " + volNames.length + " volumes"); - let text = ""; + for (let i = 0; i < volNames.length; i++) { let name = volNames.queryElementAt(i, Ci.nsISupportsString); let volume = volumeService.getVolumeByName(name.data); log("Got volume: " + name.data + " at " + volume.mountPoint); - text += "wipe " + volume.mountPoint + "\n"; + commands.push("wipe " + volume.mountPoint); } + } else if (reason == "root") { + commands.push("root"); + } + if (commands.length > 0) { Cu.import("resource://gre/modules/osfile.jsm"); let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); dir.initWithPath("/persist"); @@ -101,6 +106,7 @@ OS.Path.join("/persist", gFactoryResetFile): OS.Path.join("/cache", gFactoryResetFile); let encoder = new TextEncoder(); + let text = commands.join("\n"); let array = encoder.encode(text); let promise = OS.File.writeAtomic(postResetFile, array, { tmpPath: postResetFile + ".tmp" }); diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/dolphin/sources.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/dolphin/sources.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/dolphin/sources.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/dolphin/sources.xml 2014-12-15 12:28:54.000000000 +0000 @@ -15,7 +15,7 @@ - + @@ -23,7 +23,7 @@ - + @@ -116,7 +116,7 @@ - + @@ -135,7 +135,7 @@ - + diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/emulator/sources.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/emulator/sources.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/emulator/sources.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/emulator/sources.xml 2014-12-15 12:28:54.000000000 +0000 @@ -19,13 +19,13 @@ - + - + - + diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/emulator-ics/sources.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/emulator-ics/sources.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/emulator-ics/sources.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/emulator-ics/sources.xml 2014-12-15 12:28:54.000000000 +0000 @@ -19,13 +19,13 @@ - + - + - + diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/emulator-jb/sources.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/emulator-jb/sources.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/emulator-jb/sources.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/emulator-jb/sources.xml 2014-12-15 12:28:54.000000000 +0000 @@ -17,10 +17,10 @@ - + - + @@ -118,7 +118,7 @@ - + diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/emulator-kk/sources.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/emulator-kk/sources.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/emulator-kk/sources.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/emulator-kk/sources.xml 2014-12-15 12:28:54.000000000 +0000 @@ -15,7 +15,7 @@ - + @@ -23,7 +23,7 @@ - + @@ -116,7 +116,7 @@ - + @@ -130,7 +130,7 @@ - + diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/flame/sources.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/flame/sources.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/flame/sources.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/flame/sources.xml 2014-12-15 12:28:54.000000000 +0000 @@ -17,10 +17,10 @@ - + - + diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/flame-kk/sources.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/flame-kk/sources.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/flame-kk/sources.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/flame-kk/sources.xml 2014-12-15 12:28:54.000000000 +0000 @@ -15,7 +15,7 @@ - + @@ -23,7 +23,7 @@ - + @@ -116,13 +116,14 @@ - + + @@ -136,8 +137,8 @@ - - + + diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/gaia.json thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/gaia.json --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/gaia.json 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/gaia.json 2014-12-15 12:28:54.000000000 +0000 @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "5f064a62ce5ee63c6f75ff3422a7472bec71f7bf", + "revision": "f741dca42f03f16982c026484be42126a60a8adc", "repo_path": "integration/gaia-central" } diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/hamachi/sources.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/hamachi/sources.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/hamachi/sources.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/hamachi/sources.xml 2014-12-15 12:28:54.000000000 +0000 @@ -17,11 +17,11 @@ - + - + diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/helix/sources.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/helix/sources.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/helix/sources.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/helix/sources.xml 2014-12-15 12:28:54.000000000 +0000 @@ -15,7 +15,7 @@ - + diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/nexus-4/sources.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/nexus-4/sources.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/nexus-4/sources.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/nexus-4/sources.xml 2014-12-15 12:28:54.000000000 +0000 @@ -17,10 +17,10 @@ - + - + @@ -118,7 +118,7 @@ - + diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/wasabi/sources.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/wasabi/sources.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/config/wasabi/sources.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/config/wasabi/sources.xml 2014-12-15 12:28:54.000000000 +0000 @@ -17,12 +17,12 @@ - + - + diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/confvars.sh thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/confvars.sh --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/confvars.sh 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/confvars.sh 2014-12-15 12:28:54.000000000 +0000 @@ -65,5 +65,4 @@ MOZ_BUNDLED_FONTS=1 -export JSGC_GENERATIONAL=1 export JS_GC_SMALL_CHUNK_SIZE=1 diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/simulator/lib/main.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/simulator/lib/main.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/simulator/lib/main.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/simulator/lib/main.js 2014-12-15 12:28:54.000000000 +0000 @@ -5,21 +5,58 @@ const { Cc, Ci, Cu } = require("chrome"); -const { SimulatorProcess } = require("./simulator-process"); +const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {}); const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); const { Simulator } = Cu.import("resource://gre/modules/devtools/Simulator.jsm"); -const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {}); +const { SimulatorProcess } = require("./simulator-process"); +const Runtime = require("sdk/system/runtime"); +const URL = require("sdk/url"); + +const ROOT_URI = require("addon").uri; +const PROFILE_URL = ROOT_URI + "profile/"; +const BIN_URL = ROOT_URI + "b2g/"; let process; -function launch({ port }) { - // Close already opened simulation +function launch(options) { + // Close already opened simulation. if (process) { - return close().then(launch.bind(null, { port: port })); + return close().then(launch.bind(null, options)); + } + + // Compute B2G runtime path. + let path; + try { + let pref = "extensions." + require("addon").id + ".customRuntime"; + path = Services.prefs.getComplexValue(pref, Ci.nsIFile); + } catch(e) {} + + if (!path) { + let executables = { + WINNT: "b2g-bin.exe", + Darwin: "B2G.app/Contents/MacOS/b2g-bin", + Linux: "b2g-bin", + }; + path = URL.toFilename(BIN_URL); + path += Runtime.OS == "WINNT" ? "\\" : "/"; + path += executables[Runtime.OS]; + } + options.runtimePath = path; + console.log("simulator path:", options.runtimePath); + + // Compute Gaia profile path. + if (!options.profilePath) { + let gaiaProfile; + try { + let pref = "extensions." + require("addon").id + ".gaiaProfile"; + gaiaProfile = Services.prefs.getComplexValue(pref, Ci.nsIFile).path; + } catch(e) {} + + options.profilePath = gaiaProfile || URL.toFilename(PROFILE_URL); } - process = SimulatorProcess(); - process.remoteDebuggerPort = port; + process = new SimulatorProcess(options); process.run(); return promise.resolve(); diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/simulator/lib/simulator-process.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/simulator/lib/simulator-process.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/b2g/simulator/lib/simulator-process.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/b2g/simulator/lib/simulator-process.js 2014-12-15 12:28:54.000000000 +0000 @@ -9,18 +9,12 @@ Cu.import("resource://gre/modules/Services.jsm"); -const { EventTarget } = require("sdk/event/target"); -const { emit, off } = require("sdk/event/core"); -const { Class } = require("sdk/core/heritage"); const Environment = require("sdk/system/environment").env; const Runtime = require("sdk/system/runtime"); -const URL = require("sdk/url"); const Subprocess = require("sdk/system/child_process/subprocess"); const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); +const { EventEmitter } = Cu.import("resource://gre/modules/devtools/event-emitter.js", {}); -const ROOT_URI = require("addon").uri; -const PROFILE_URL = ROOT_URI + "profile/"; -const BIN_URL = ROOT_URI + "b2g/"; // Log subprocess error and debug messages to the console. This logs messages // for all consumers of the API. We trim the messages because they sometimes @@ -33,14 +27,15 @@ function(s) console.debug("subprocess: " + s.trim()) ); -exports.SimulatorProcess = Class({ - extends: EventTarget, - initialize: function initialize(options) { - EventTarget.prototype.initialize.call(this, options); +function SimulatorProcess(options) { + this.options = options; - this.on("stdout", function onStdout(data) console.log(data.trim())); - this.on("stderr", function onStderr(data) console.error(data.trim())); - }, + EventEmitter.decorate(this); + this.on("stdout", data => { console.log(data.trim()) }); + this.on("stderr", data => { console.error(data.trim()) }); +} + +SimulatorProcess.prototype = { // check if b2g is running get isRunning() !!this.process, @@ -77,7 +72,7 @@ let environment; if (Runtime.OS == "Linux") { - environment = ["TMPDIR=" + Services.dirsvc.get("TmpD",Ci.nsIFile).path]; + environment = ["TMPDIR=" + Services.dirsvc.get("TmpD", Ci.nsIFile).path]; if ("DISPLAY" in Environment) { environment.push("DISPLAY=" + Environment.DISPLAY); } @@ -90,21 +85,21 @@ environment: environment, // emit stdout event - stdout: (function(data) { - emit(this, "stdout", data); - }).bind(this), + stdout: data => { + this.emit("stdout", data); + }, // emit stderr event - stderr: (function(data) { - emit(this, "stderr", data); - }).bind(this), + stderr: data => { + this.emit("stderr", data); + }, - // on b2g instance exit, reset tracked process, remoteDebuggerPort and + // on b2g instance exit, reset tracked process, remote debugger port and // shuttingDown flag, then finally emit an exit event done: (function(result) { - console.log(this.b2gFilename + " terminated with " + result.exitCode); + console.log("B2G terminated with " + result.exitCode); this.process = null; - emit(this, "exit", result.exitCode); + this.emit("exit", result.exitCode); }).bind(this) }); }, @@ -119,7 +114,7 @@ }); if (!this.shuttingDown) { this.shuttingDown = true; - emit(this, "kill", null); + this.emit("kill", null); this.process.kill(); } return deferred.promise; @@ -128,42 +123,14 @@ } }, - // compute current b2g filename - get b2gFilename() { - return this._executable ? this._executableFilename : "B2G"; - }, - // compute current b2g file handle get b2gExecutable() { if (this._executable) { return this._executable; } - let customRuntime; - try { - let pref = "extensions." + require("addon").id + ".customRuntime"; - customRuntime = Services.prefs.getComplexValue(pref, Ci.nsIFile); - } catch(e) {} - - if (customRuntime) { - this._executable = customRuntime; - this._executableFilename = "Custom runtime"; - return this._executable; - } - - let bin = URL.toFilename(BIN_URL); - let executables = { - WINNT: "b2g-bin.exe", - Darwin: "B2G.app/Contents/MacOS/b2g-bin", - Linux: "b2g-bin", - }; - - let path = bin; - path += Runtime.OS == "WINNT" ? "\\" : "/"; - path += executables[Runtime.OS]; - console.log("simulator path: " + path); let executable = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - executable.initWithPath(path); + executable.initWithPath(this.options.runtimePath); if (!executable.exists()) { // B2G binaries not found @@ -171,7 +138,6 @@ } this._executable = executable; - this._executableFilename = "b2g-bin"; return executable; }, @@ -180,23 +146,18 @@ get b2gArguments() { let args = []; - let gaiaProfile; - try { - let pref = "extensions." + require("addon").id + ".gaiaProfile"; - gaiaProfile = Services.prefs.getComplexValue(pref, Ci.nsIFile).path; - } catch(e) {} - - let profile = gaiaProfile || URL.toFilename(PROFILE_URL); + let profile = this.options.profilePath; args.push("-profile", profile); console.log("profile", profile); // NOTE: push dbgport option on the b2g-desktop commandline - args.push("-start-debugger-server", "" + this.remoteDebuggerPort); + args.push("-start-debugger-server", "" + this.options.port); // Ignore eventual zombie instances of b2g that are left over args.push("-no-remote"); return args; }, -}); +}; +exports.SimulatorProcess = SimulatorProcess; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/app/profile/firefox.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/app/profile/firefox.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/app/profile/firefox.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/app/profile/firefox.js 2014-12-15 12:28:54.000000000 +0000 @@ -1506,9 +1506,9 @@ pref("devtools.gcli.eagerHelper", 2); // Alias to the script URLs for inject command. -pref("devtools.gcli.jquerySrc", "http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"); -pref("devtools.gcli.lodashSrc", "http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"); -pref("devtools.gcli.underscoreSrc", "http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"); +pref("devtools.gcli.jquerySrc", "https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"); +pref("devtools.gcli.lodashSrc", "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"); +pref("devtools.gcli.underscoreSrc", "https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"); // Remember the Web Console filters pref("devtools.webconsole.filter.network", true); diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/abouthome/aboutHome.xhtml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/abouthome/aboutHome.xhtml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/abouthome/aboutHome.xhtml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/abouthome/aboutHome.xhtml 2014-12-15 12:28:54.000000000 +0000 @@ -64,7 +64,7 @@ - + #ifdef XP_WIN diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser.css thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser.css --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser.css 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser.css 2014-12-15 12:28:54.000000000 +0000 @@ -979,7 +979,7 @@ /* Combobox dropdown renderer */ #ContentSelectDropdown { - max-height: 400px; + -moz-binding: url("chrome://global/content/bindings/popup.xml#popup-scrollbars"); } .contentSelectDropdown-optgroup { diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser-doctype.inc thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser-doctype.inc --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser-doctype.inc 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser-doctype.inc 2014-12-15 12:28:54.000000000 +0000 @@ -21,5 +21,7 @@ %aboutHomeDTD; %searchBarDTD; + +%syncBrandDTD; ]> diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser-fxaccounts.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser-fxaccounts.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser-fxaccounts.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser-fxaccounts.js 2014-12-15 12:28:54.000000000 +0000 @@ -6,8 +6,12 @@ return Cu.import("resource://gre/modules/FxAccountsCommon.js", {}); }); +XPCOMUtils.defineLazyModuleGetter(this, "fxaMigrator", + "resource://services-sync/FxaMigrator.jsm"); + const PREF_SYNC_START_DOORHANGER = "services.sync.ui.showSyncStartDoorhanger"; const DOORHANGER_ACTIVATE_DELAY_MS = 5000; +const SYNC_MIGRATION_NOTIFICATION_TITLE = "fxa-migration"; let gFxAccounts = { @@ -29,6 +33,7 @@ "weave:service:sync:start", "weave:service:login:error", "weave:service:setup-complete", + "fxa-migration:state-changed", FxAccountsCommon.ONVERIFIED_NOTIFICATION, FxAccountsCommon.ONLOGOUT_NOTIFICATION ]; @@ -39,6 +44,13 @@ return this.button = document.getElementById("PanelUI-fxa-status"); }, + get strings() { + delete this.strings; + return this.strings = Services.strings.createBundle( + "chrome://browser/locale/accounts.properties" + ); + }, + get loginFailed() { // Referencing Weave.Service will implicitly initialize sync, and we don't // want to force that - so first check if it is ready. @@ -73,6 +85,10 @@ gNavToolbox.addEventListener("customizationstarting", this); gNavToolbox.addEventListener("customizationending", this); + // Request the current Legacy-Sync-to-FxA migration status. We'll be + // notified of fxa-migration:state-changed in response if necessary. + Services.obs.notifyObservers(null, "fxa-migration:state-request", null); + this._initialized = true; this.updateUI(); @@ -90,7 +106,7 @@ this._initialized = false; }, - observe: function (subject, topic) { + observe: function (subject, topic, data) { switch (topic) { case FxAccountsCommon.ONVERIFIED_NOTIFICATION: Services.prefs.setBoolPref(PREF_SYNC_START_DOORHANGER, true); @@ -98,6 +114,9 @@ case "weave:service:sync:start": this.onSyncStart(); break; + case "fxa-migration:state-changed": + this.onMigrationStateChanged(data, subject); + break; default: this.updateUI(); break; @@ -121,6 +140,14 @@ } }, + onMigrationStateChanged: function (newState, email) { + this._migrationInfo = !newState ? null : { + state: newState, + email: email ? email.QueryInterface(Ci.nsISupportsString).data : null, + }; + this.updateUI(); + }, + handleEvent: function (event) { if (event.type == "activate") { // Our window might have been in the background while we received the @@ -131,7 +158,7 @@ setTimeout(() => this.onSyncStart(), DOORHANGER_ACTIVATE_DELAY_MS); } else { this._inCustomizationMode = event.type == "customizationstarting"; - this.updateUI(); + this.updateAppMenuItem(); } }, @@ -156,13 +183,29 @@ }, updateUI: function () { + this.updateAppMenuItem(); + this.updateMigrationNotification(); + }, + + updateAppMenuItem: function () { + if (this._migrationInfo) { + this.updateAppMenuItemForMigration(); + return; + } + // Bail out if FxA is disabled. if (!this.weave.fxAccountsEnabled) { + // When migration transitions from needs-verification to the null state, + // fxAccountsEnabled is false because migration has not yet finished. In + // that case, hide the button. We'll get another notification with a null + // state once migration is complete. + this.button.hidden = true; + this.button.removeAttribute("fxastatus"); return; } // FxA is enabled, show the widget. - this.button.removeAttribute("hidden"); + this.button.hidden = false; // Make sure the button is disabled in customization mode. if (this._inCustomizationMode) { @@ -180,15 +223,14 @@ // Reset the button to its original state. this.button.setAttribute("label", defaultLabel); this.button.removeAttribute("tooltiptext"); - this.button.removeAttribute("signedin"); - this.button.removeAttribute("failed"); + this.button.removeAttribute("fxastatus"); if (!this._inCustomizationMode) { if (this.loginFailed) { - this.button.setAttribute("failed", "true"); + this.button.setAttribute("fxastatus", "error"); this.button.setAttribute("label", errorLabel); } else if (userData) { - this.button.setAttribute("signedin", "true"); + this.button.setAttribute("fxastatus", "signedin"); this.button.setAttribute("label", userData.email); this.button.setAttribute("tooltiptext", userData.email); } @@ -205,15 +247,95 @@ }); }, + updateAppMenuItemForMigration: Task.async(function* () { + let status = null; + let label = null; + switch (this._migrationInfo.state) { + case fxaMigrator.STATE_USER_FXA: + status = "migrate-signup"; + label = this.strings.formatStringFromName("needUserShort", + [this.button.getAttribute("fxabrandname")], 1); + break; + case fxaMigrator.STATE_USER_FXA_VERIFIED: + status = "migrate-verify"; + label = this.strings.formatStringFromName("needVerifiedUserShort", + [this._migrationInfo.email], + 1); + break; + } + this.button.label = label; + this.button.hidden = false; + this.button.setAttribute("fxastatus", status); + }), + + updateMigrationNotification: Task.async(function* () { + if (!this._migrationInfo) { + Weave.Notifications.removeAll(SYNC_MIGRATION_NOTIFICATION_TITLE); + return; + } + if (gBrowser.currentURI.spec.split("?")[0] == "about:accounts") { + // If the current tab is about:accounts, assume the user just completed a + // migration step and don't bother them with a redundant notification. + return; + } + let note = null; + switch (this._migrationInfo.state) { + case fxaMigrator.STATE_USER_FXA: { + let msg = this.strings.GetStringFromName("needUserLong"); + let upgradeLabel = + this.strings.GetStringFromName("upgradeToFxA.label"); + let upgradeAccessKey = + this.strings.GetStringFromName("upgradeToFxA.accessKey"); + note = new Weave.Notification( + undefined, msg, undefined, Weave.Notifications.PRIORITY_WARNING, [ + new Weave.NotificationButton(upgradeLabel, upgradeAccessKey, () => { + fxaMigrator.createFxAccount(window); + }), + ] + ); + break; + } + case fxaMigrator.STATE_USER_FXA_VERIFIED: { + let msg = + this.strings.formatStringFromName("needVerifiedUserLong", + [this._migrationInfo.email], 1); + let resendLabel = + this.strings.GetStringFromName("resendVerificationEmail.label"); + let resendAccessKey = + this.strings.GetStringFromName("resendVerificationEmail.accessKey"); + note = new Weave.Notification( + undefined, msg, undefined, Weave.Notifications.PRIORITY_INFO, [ + new Weave.NotificationButton(resendLabel, resendAccessKey, () => { + fxaMigrator.resendVerificationMail(); + }), + ] + ); + break; + } + } + note.title = SYNC_MIGRATION_NOTIFICATION_TITLE; + Weave.Notifications.replaceTitle(note); + }), + onMenuPanelCommand: function (event) { let button = event.originalTarget; - if (button.hasAttribute("signedin")) { + switch (button.getAttribute("fxastatus")) { + case "signedin": this.openPreferences(); - } else if (button.hasAttribute("failed")) { - this.openSignInAgainPage(); - } else { - this.openAccountsPage(); + break; + case "error": + this.openSignInAgainPage("menupanel"); + break; + case "migrate-signup": + fxaMigrator.createFxAccount(window); + break; + case "migrate-verify": + fxaMigrator.resendVerificationMail(); + break; + default: + this.openAccountsPage(null, { entryPoint: "menupanel" }); + break; } PanelUI.hide(); @@ -223,23 +345,30 @@ openPreferences("paneSync"); }, - openAccountsPage: function () { - let entryPoint = "menupanel"; - if (UITour.originTabs.get(window) && UITour.originTabs.get(window).has(gBrowser.selectedTab)) { - entryPoint = "uitour"; + openAccountsPage: function (action, urlParams={}) { + // An entryPoint param is used for server-side metrics. If the current tab + // is UITour, assume that it initiated the call to this method and override + // the entryPoint accordingly. + if (UITour.originTabs.get(window) && + UITour.originTabs.get(window).has(gBrowser.selectedTab)) { + urlParams.entryPoint = "uitour"; + } + let params = new URLSearchParams(); + if (action) { + params.set("action", action); + } + for (let name in urlParams) { + if (urlParams[name] !== undefined) { + params.set(name, urlParams[name]); + } } - switchToTabHavingURI("about:accounts?entrypoint=" + entryPoint, true, { + let url = "about:accounts?" + params; + switchToTabHavingURI(url, true, { replaceQueryString: true }); }, - openSignInAgainPage: function () { - let entryPoint = "menupanel"; - if (UITour.originTabs.get(window) && UITour.originTabs.get(window).has(gBrowser.selectedTab)) { - entryPoint = "uitour"; - } - switchToTabHavingURI("about:accounts?action=reauth&entrypoint=" + entryPoint, true, { - replaceQueryString: true - }); - } + openSignInAgainPage: function (entryPoint) { + this.openAccountsPage("reauth", { entryPoint: entryPoint }); + }, }; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser.js 2014-12-15 12:28:54.000000000 +0000 @@ -154,9 +154,6 @@ "resource://gre/modules/SafeBrowsing.jsm"); #endif -XPCOMUtils.defineLazyModuleGetter(this, "gBrowserNewTabPreloader", - "resource:///modules/BrowserNewTabPreloader.jsm", "BrowserNewTabPreloader"); - XPCOMUtils.defineLazyModuleGetter(this, "gCustomizationTabPreloader", "resource:///modules/CustomizationTabPreloader.jsm", "CustomizationTabPreloader"); @@ -2550,9 +2547,9 @@ updateButtonText = gNavigatorBundle.getFormattedString(stringId, [brandShortName]); - updateButton.label = updateButtonText; - updateButton.hidden = false; + updateButton.setAttribute("label", updateButtonText); updateButton.setAttribute("update-status", "succeeded"); + updateButton.hidden = false; PanelUI.panel.addEventListener("popupshowing", this, true); @@ -2565,9 +2562,9 @@ stringId = "appmenu.updateFailed.description"; updateButtonText = gNavigatorBundle.getString(stringId); - updateButton.label = updateButtonText; - updateButton.hidden = false; + updateButton.setAttribute("label", updateButtonText); updateButton.setAttribute("update-status", "failed"); + updateButton.hidden = false; PanelUI.panel.addEventListener("popupshowing", this, true); diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser-safebrowsing.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser-safebrowsing.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser-safebrowsing.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser-safebrowsing.js 2014-12-15 12:28:54.000000000 +0000 @@ -7,7 +7,7 @@ setReportPhishingMenu: function() { // A phishing page will have a specific about:blocked content documentURI - var uri = getBrowser().currentURI; + var uri = gBrowser.currentURI; var isPhishingPage = uri && uri.spec.startsWith("about:blocked?e=phishingBlocked"); // Show/hide the appropriate menu item. diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser-social.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser-social.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser-social.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser-social.js 2014-12-15 12:28:54.000000000 +0000 @@ -552,6 +552,7 @@ button.setAttribute("image", provider.iconURL); button.setAttribute("tooltip", "share-button-tooltip"); button.setAttribute("origin", provider.origin); + button.setAttribute("label", provider.name); button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin'));"); if (provider == selectedProvider) { this.defaultButton = button; diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser-syncui.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser-syncui.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/browser-syncui.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/browser-syncui.js 2014-12-15 12:28:54.000000000 +0000 @@ -13,8 +13,6 @@ // gSyncUI handles updating the tools menu and displaying notifications. let gSyncUI = { - DEFAULT_EOL_URL: "https://www.mozilla.org/firefox/?utm_source=synceol", - _obs: ["weave:service:sync:start", "weave:service:quota:remaining", "weave:service:setup-complete", @@ -27,7 +25,6 @@ "weave:ui:sync:error", "weave:ui:sync:finish", "weave:ui:clear-error", - "weave:eol", ], _unloaded: false, @@ -92,6 +89,10 @@ // notificationbox will listen to observers from now on. Services.obs.removeObserver(this, "weave:notification:added"); + let idx = this._obs.indexOf("weave:notification:added"); + if (idx >= 0) { + this._obs.splice(idx, 1); + } }, _needsSetup: function SUI__needsSetup() { @@ -145,12 +146,13 @@ return; let syncButton = document.getElementById("sync-button"); + if (syncButton) { + syncButton.removeAttribute("status"); + } let panelHorizontalButton = document.getElementById("PanelUI-fxa-status"); - [syncButton, panelHorizontalButton].forEach(function(button) { - if (!button) - return; - button.removeAttribute("status"); - }); + if (panelHorizontalButton) { + panelHorizontalButton.removeAttribute("syncstatus"); + } if (needsSetup && syncButton) syncButton.removeAttribute("tooltiptext"); @@ -164,12 +166,14 @@ if (!gBrowser) return; - ["sync-button", "PanelUI-fxa-status"].forEach(function(id) { - let button = document.getElementById(id); - if (!button) - return; + let button = document.getElementById("sync-button"); + if (button) { button.setAttribute("status", "active"); - }); + } + button = document.getElementById("PanelUI-fxa-status"); + if (button) { + button.setAttribute("syncstatus", "active"); + } }, onLoginFinish: function SUI_onLoginFinish() { @@ -253,32 +257,6 @@ return brand.get("brandShortName"); }, - onEOLNotice: function (data) { - let code = data.code; - let kind = (code == "hard-eol") ? "error" : "warning"; - let url = data.url || gSyncUI.DEFAULT_EOL_URL; - - let title = this._stringBundle.GetStringFromName(kind + ".sync.eol.label"); - let description = this._stringBundle.formatStringFromName(kind + ".sync.eol.description", - [this._getAppName()], - 1); - - let buttons = []; - buttons.push(new Weave.NotificationButton( - this._stringBundle.GetStringFromName("sync.eol.learnMore.label"), - this._stringBundle.GetStringFromName("sync.eol.learnMore.accesskey"), - function() { - window.openUILinkIn(url, "tab"); - return true; - } - )); - - let priority = (kind == "error") ? Weave.Notifications.PRIORITY_WARNING : - Weave.Notifications.PRIORITY_INFO; - let notification = new Weave.Notification(title, description, null, priority, buttons); - Weave.Notifications.replaceTitle(notification); - }, - openServerStatus: function () { let statusURL = Services.prefs.getCharPref("services.sync.statusURL"); window.openUILinkIn(statusURL, "tab"); @@ -374,13 +352,7 @@ }, openSignInAgainPage: function (entryPoint = "syncbutton") { - // If the user is also in an uitour, set the entrypoint to `uitour` - if (UITour.originTabs.get(window) && UITour.originTabs.get(window).has(gBrowser.selectedTab)) { - entryPoint = "uitour"; - } - switchToTabHavingURI("about:accounts?action=reauth&entrypoint=" + entryPoint, true, { - replaceQueryString: true - }); + gFxAccounts.openSignInAgainPage(entryPoint); }, // Helpers @@ -550,9 +522,6 @@ case "weave:ui:clear-error": this.clearError(); break; - case "weave:eol": - this.onEOLNotice(subject); - break; } }, diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/content.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/content.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/content.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/content.js 2014-12-15 12:28:54.000000000 +0000 @@ -327,12 +327,6 @@ doc.documentElement.setAttribute("searchUIConfiguration", "oldsearchui"); } - // XXX bug 738646 - when Marketplace is launched, remove this statement and - // the hidden attribute set on the apps button in aboutHome.xhtml - if (Services.prefs.getPrefType("browser.aboutHome.apps") == Services.prefs.PREF_BOOL && - Services.prefs.getBoolPref("browser.aboutHome.apps")) - doc.getElementById("apps").removeAttribute("hidden"); - sendAsyncMessage("AboutHome:RequestUpdate"); doc.addEventListener("AboutHomeSearchEvent", this, true, true); doc.addEventListener("AboutHomeSearchPanel", this, true, true); diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/socialchat.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/socialchat.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/socialchat.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/socialchat.xml 2014-12-15 12:28:54.000000000 +0000 @@ -53,6 +53,14 @@ this.isActive = !this.minimized; this._deferredChatLoaded.resolve(this); }, true); + + // load content.js to support webrtc, fullscreen, etc. + this.addEventListener("load", function loaded(event) { + this.removeEventListener("load", loaded, true); + let mm = this.content.messageManager; + mm.loadFrameScript("chrome://browser/content/content.js", true); + }, true); + if (this.src) this.setAttribute("src", this.src); ]]> @@ -661,6 +669,12 @@ if (event.target != otherWin.document) return; + let detachEvent = new aChatbox.contentWindow.CustomEvent("socialFrameDetached", { + bubbles: true, + cancelable: true, + }); + aChatbox.contentDocument.dispatchEvent(detachEvent); + otherWin.removeEventListener("load", _chatLoad, true); let otherChatbox = otherWin.document.getElementById("chatter"); aChatbox.swapDocShells(otherChatbox); diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/tabbrowser.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/tabbrowser.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/tabbrowser.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/tabbrowser.xml 2014-12-15 12:28:54.000000000 +0000 @@ -1513,6 +1513,134 @@ + null + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1583,55 +1711,23 @@ if (aOwner) t.owner = aOwner; - var b = document.createElementNS( - "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", - "browser"); - b.setAttribute("type", "content-targetable"); - b.setAttribute("message", "true"); - b.setAttribute("messagemanagergroup", "browsers"); - b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu")); - b.setAttribute("tooltip", this.getAttribute("contenttooltip")); - - if (remote) - b.setAttribute("remote", "true"); + let b; + let usingPreloadedContent = false; + let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window); - if (window.gShowPageResizers && window.windowState == window.STATE_NORMAL) { - b.setAttribute("showresizer", "true"); + // If we open a new tab with the newtab URL, + // check if there is a preloaded browser ready. + if (aURI == BROWSER_NEW_TAB_URL && !isPrivateWindow) { + b = this._getPreloadedBrowser(); + usingPreloadedContent = !!b; } - if (this.hasAttribute("autocompletepopup")) - b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup")); - - if (this.hasAttribute("selectpopup")) - b.setAttribute("selectpopup", this.getAttribute("selectpopup")); - - b.setAttribute("autoscrollpopup", this._autoScrollPopup.id); - - // Create the browserStack container - var stack = document.createElementNS(NS_XUL, "stack"); - stack.className = "browserStack"; - stack.appendChild(b); - stack.setAttribute("flex", "1"); - - // Create the browserContainer - var browserContainer = document.createElementNS(NS_XUL, "vbox"); - browserContainer.className = "browserContainer"; - browserContainer.appendChild(stack); - browserContainer.setAttribute("flex", "1"); - - // Create the sidebar container - var browserSidebarContainer = document.createElementNS(NS_XUL, - "hbox"); - browserSidebarContainer.className = "browserSidebarContainer"; - browserSidebarContainer.appendChild(browserContainer); - browserSidebarContainer.setAttribute("flex", "1"); - - // Add the Message and the Browser to the box - var notificationbox = document.createElementNS(NS_XUL, - "notificationbox"); - notificationbox.setAttribute("flex", "1"); - notificationbox.appendChild(browserSidebarContainer); + if (!b) { + // No preloaded browser found, create one. + b = this._createBrowser({remote, uriIsAboutBlank}); + } + let notificationbox = this.getNotificationBox(b); var position = this.tabs.length - 1; var uniqueId = this._generateUniquePanelID(); notificationbox.id = uniqueId; @@ -1642,19 +1738,16 @@ t.lastAccessed = Date.now(); this.tabContainer._setPositionalAttributes(); - // Prevent the superfluous initial load of a blank document - // if we're going to load something other than about:blank. - if (!uriIsAboutBlank) { - b.setAttribute("nodefaultsrc", "true"); + // Inject the into the DOM if necessary. + if (!notificationbox.parentNode) { + // NB: this appendChild call causes us to run constructors for the + // browser element, which fires off a bunch of notifications. Some + // of those notifications can cause code to run that inspects our + // state, so it is important that the tab element is fully + // initialized by this point. + this.mPanelContainer.appendChild(notificationbox); } - // NB: this appendChild call causes us to run constructors for the - // browser element, which fires off a bunch of notifications. Some - // of those notifications can cause code to run that inspects our - // state, so it is important that the tab element is fully - // initialized by this point. - this.mPanelContainer.appendChild(notificationbox); - // We've waited until the tab is in the DOM to set the label. This // allows the TabLabelModified event to be properly dispatched. if (!aURI || isBlankPageURL(aURI)) { @@ -1677,16 +1770,9 @@ b.droppedLinkHandler = handleDroppedLink; - // If we just created a new tab that loads the default - // newtab url, swap in a preloaded page if possible. - // Do nothing if we're a private window. - let docShellsSwapped = false; - if (aURI == BROWSER_NEW_TAB_URL && - !PrivateBrowsingUtils.isWindowPrivate(window) && - !gMultiProcessBrowser) { - docShellsSwapped = gBrowserNewTabPreloader.newTab(t); - } else if (aURI == "about:customizing") { - docShellsSwapped = gCustomizationTabPreloader.newTab(t); + // Swap in a preloaded customize tab, if available. + if (aURI == "about:customizing") { + usingPreloadedContent = gCustomizationTabPreloader.newTab(t); } // Dispatch a new tab notification. We do this once we're @@ -1698,7 +1784,7 @@ // If we didn't swap docShells with a preloaded browser // then let's just continue loading the page normally. - if (!docShellsSwapped && !uriIsAboutBlank) { + if (!usingPreloadedContent && !uriIsAboutBlank) { // pretend the user typed this so it'll be available till // the document successfully loads if (aURI && gInitialPages.indexOf(aURI) == -1) @@ -4279,6 +4365,9 @@ // without any scrolling and when the tabbar has already // overflowed. this.mTabstrip._updateScrollButtonsDisabledState(); + + // Preload the next about:newtab if there isn't one already. + this.tabbrowser._createPreloadBrowser(); ]]> diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/test/chat/browser_chatwindow.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/test/chat/browser_chatwindow.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/test/chat/browser_chatwindow.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/test/chat/browser_chatwindow.js 2014-12-15 12:28:54.000000000 +0000 @@ -6,6 +6,30 @@ let chatbar = document.getElementById("pinnedchats"); +function waitForCondition(condition, errorMsg) { + let deferred = Promise.defer(); + var tries = 0; + var interval = setInterval(function() { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + var conditionPassed; + try { + conditionPassed = condition(); + } catch (e) { + ok(false, e + "\n" + e.stack); + conditionPassed = false; + } + if (conditionPassed) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { clearInterval(interval); deferred.resolve(); }; + return deferred.promise; +} + add_chat_task(function* testOpenCloseChat() { let chatbox = yield promiseOpenChat("http://example.com"); Assert.strictEqual(chatbox, chatbar.selectedChat); @@ -99,11 +123,15 @@ // therefore be the window that hosts the new chat (see bug 835111) let secondWindow = OpenBrowserWindow(); yield promiseOneEvent(secondWindow, "load"); + Assert.equal(secondWindow, Chat.findChromeWindowForChats(null), "Second window is the preferred chat window"); Assert.equal(numChatsInWindow(secondWindow), 0, "second window starts with no chats"); yield promiseOpenChat("http://example.com#2"); Assert.equal(numChatsInWindow(secondWindow), 1, "second window now has chats"); Assert.equal(numChatsInWindow(window), 1, "first window still has 1 chat"); chat.close(); + + // a bit heavy handed, but handy fixing bug 1090633 + yield waitForCondition(function () !chat.parentNode, "chat has been detached"); Assert.equal(numChatsInWindow(window), 0, "first window now has no chats"); // now open another chat - it should still open in the second. yield promiseOpenChat("http://example.com#3"); diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/test/general/browser_fxa_migrate.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/test/general/browser_fxa_migrate.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/test/general/browser_fxa_migrate.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/test/general/browser_fxa_migrate.js 2014-12-15 12:28:54.000000000 +0000 @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const STATE_CHANGED_TOPIC = "fxa-migration:state-changed"; +const NOTIFICATION_TITLE = "fxa-migration"; + +let imports = {}; +Cu.import("resource://services-sync/FxaMigrator.jsm", imports); + +add_task(function* test() { + // Fake the state where we need an FxA user. + let buttonPromise = promiseButtonMutation(); + Services.obs.notifyObservers(null, STATE_CHANGED_TOPIC, + fxaMigrator.STATE_USER_FXA); + let buttonState = yield buttonPromise; + assertButtonState(buttonState, "migrate-signup", true); + Assert.ok(Weave.Notifications.notifications.some(n => { + return n.title == NOTIFICATION_TITLE; + }), "Needs-user notification should be present"); + + // Fake the state where we need a verified FxA user. + buttonPromise = promiseButtonMutation(); + let email = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + email.data = "foo@example.com"; + Services.obs.notifyObservers(email, STATE_CHANGED_TOPIC, + fxaMigrator.STATE_USER_FXA_VERIFIED); + buttonState = yield buttonPromise; + assertButtonState(buttonState, "migrate-verify", true, + "foo@example.com not verified"); + let note = Weave.Notifications.notifications.find(n => { + return n.title == NOTIFICATION_TITLE; + }); + Assert.ok(!!note, "Needs-verification notification should be present"); + Assert.ok(note.description.contains(email.data), + "Needs-verification notification should include email"); + + // Fake the state where no migration is needed. + buttonPromise = promiseButtonMutation(); + Services.obs.notifyObservers(null, STATE_CHANGED_TOPIC, null); + buttonState = yield buttonPromise; + // In this case, the front end has called fxAccounts.getSignedInUser() to + // update the button label and status. But since there isn't actually a user, + // the button is left with no fxastatus. + assertButtonState(buttonState, "", true); + Assert.ok(!Weave.Notifications.notifications.some(n => { + return n.title == NOTIFICATION_TITLE; + }), "Migration notifications should no longer be present"); +}); + +function assertButtonState(buttonState, expectedStatus, expectedVisible, + expectedLabel=undefined) { + Assert.equal(buttonState.fxastatus, expectedStatus, + "Button fxstatus attribute"); + Assert.equal(!buttonState.hidden, expectedVisible, "Button visibility"); + if (expectedLabel !== undefined) { + Assert.equal(buttonState.label, expectedLabel, "Button label"); + } +} + +function promiseButtonMutation() { + return new Promise((resolve, reject) => { + let obs = new MutationObserver(mutations => { + info("Observed mutations for attributes: " + + mutations.map(m => m.attributeName)); + if (mutations.some(m => m.attributeName == "fxastatus")) { + obs.disconnect(); + resolve({ + fxastatus: gFxAccounts.button.getAttribute("fxastatus"), + hidden: gFxAccounts.button.hidden, + label: gFxAccounts.button.label, + }); + } + }); + obs.observe(gFxAccounts.button, { attributes: true }); + }); +} diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/test/general/browser.ini thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/test/general/browser.ini --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/test/general/browser.ini 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/test/general/browser.ini 2014-12-15 12:28:54.000000000 +0000 @@ -322,6 +322,7 @@ [browser_findbarClose.js] [browser_fullscreen-window-open.js] skip-if = buildapp == 'mulet' || e10s || os == "linux" # Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly. Linux: Intermittent failures - bug 941575. +[browser_fxa_migrate.js] [browser_fxa_oauth.js] [browser_gestureSupport.js] skip-if = e10s # Bug 863514 - no gesture support. diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/test/general/head.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/test/general/head.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/test/general/head.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/test/general/head.js 2014-12-15 12:28:54.000000000 +0000 @@ -665,6 +665,19 @@ } if (!("nsISystemStatusBar" in Ci)) { + if (!expected) { + let win = Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator"); + if (win) { + yield new Promise((resolve, reject) => { + win.addEventListener("unload", (e) => { + if (e.target == win.document) { + win.removeEventListener("unload", arguments.callee); + resolve(); + } + }, false); + }); + } + } let indicator = Services.wm.getEnumerator("Browser:WebRTCGlobalIndicator"); let hasWindow = indicator.hasMoreElements(); is(hasWindow, !!expected, "popup " + msg); diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/test/newtab/browser_newtab_background_captures.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/test/newtab/browser_newtab_background_captures.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/test/newtab/browser_newtab_background_captures.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/test/newtab/browser_newtab_background_captures.js 2014-12-15 12:28:54.000000000 +0000 @@ -11,7 +11,6 @@ function runTests() { let imports = {}; Cu.import("resource://gre/modules/PageThumbs.jsm", imports); - Cu.import("resource:///modules/BrowserNewTabPreloader.jsm", imports); // Disable captures. let originalDisabledState = Services.prefs.getBoolPref(CAPTURE_PREF); @@ -31,32 +30,15 @@ yield setLinks("-1"); // We need a handle to a hidden, pre-loaded newtab so we can verify that it - // doesn't allow background captures. Add a newtab, which triggers creation - // of a hidden newtab, and then keep calling BrowserNewTabPreloader.newTab - // until it returns true, meaning that it swapped the passed-in tab's docshell - // for the hidden newtab docshell. - let tab = gWindow.gBrowser.addTab("about:blank"); - yield addNewTabPageTab(); - - // When newtab is loaded very quickly (which is what happens in 99% of cases) - // there is no need to wait so no tests are run. Because each test requires - // either a pass, fail or todo we run a dummy test here. - ok(true, "Each test requires at least one pass, fail or todo so here is a pass."); - - let swapWaitCount = 0; - let swapped = imports.BrowserNewTabPreloader.newTab(tab); - while (!swapped) { - if (++swapWaitCount == 10) { - ok(false, "Timed out waiting for newtab docshell swap."); - return; - } - // Give the hidden newtab some time to finish loading. - yield wait(2000); - info("Checking newtab swap " + swapWaitCount); - swapped = imports.BrowserNewTabPreloader.newTab(tab); - } + // doesn't allow background captures. Ensure we have a preloaded browser. + gBrowser._createPreloadBrowser(); + + // Wait for the preloaded browser to load. + yield waitForBrowserLoad(gBrowser._preloadedBrowser); - // The tab's docshell is now the previously hidden newtab docshell. + // We're now ready to use the preloaded browser. + BrowserOpenTab(); + let tab = gBrowser.selectedTab; let doc = tab.linkedBrowser.contentDocument; // Enable captures. @@ -67,8 +49,11 @@ if (data != url) return; Services.obs.removeObserver(onCreate, "page-thumbnail:create"); + ok(true, "thumbnail created after preloaded tab was shown"); + // Test finished! Services.prefs.setBoolPref(CAPTURE_PREF, originalDisabledState); + gBrowser.removeTab(tab); file.remove(false); TestRunner.next(); }, "page-thumbnail:create", false); @@ -76,7 +61,3 @@ info("Waiting for thumbnail capture"); yield true; } - -function wait(ms) { - setTimeout(TestRunner.next, ms); -} diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/test/newtab/head.js thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/test/newtab/head.js --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/test/newtab/head.js 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/test/newtab/head.js 2014-12-15 12:28:54.000000000 +0000 @@ -374,19 +374,25 @@ } } - // The new tab page might have been preloaded in the background. - if (browser.contentDocument.readyState == "complete") { + // Wait for the new tab page to be loaded. + waitForBrowserLoad(browser, function () { + // Wait for the document to become visible in case it was preloaded. waitForCondition(() => !browser.contentDocument.hidden).then(whenNewTabLoaded); - return deferred.promise; + }); + + return deferred.promise; +} + +function waitForBrowserLoad(browser, callback = TestRunner.next) { + if (browser.contentDocument.readyState == "complete") { + executeSoon(callback); + return; } - // Wait for the new tab page to be loaded. browser.addEventListener("load", function onLoad() { browser.removeEventListener("load", onLoad, true); - whenNewTabLoaded(); + executeSoon(callback); }, true); - - return deferred.promise; } /** diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/urlbarBindings.xml thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/urlbarBindings.xml --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/base/content/urlbarBindings.xml 2014-12-08 19:31:01.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/base/content/urlbarBindings.xml 2014-12-15 12:28:54.000000000 +0000 @@ -960,7 +960,8 @@ - + 0) { const kBundleURI = "chrome://browser/locale/search.properties"; let bundle = Services.strings.createBundle(kBundleURI); diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/components/about/AboutRedirector.cpp thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/components/about/AboutRedirector.cpp --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/components/about/AboutRedirector.cpp 2014-12-08 19:31:02.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/components/about/AboutRedirector.cpp 2014-12-15 12:28:55.000000000 +0000 @@ -153,8 +153,22 @@ for (int i = 0; i < kRedirTotal; i++) { if (!strcmp(path.get(), kRedirMap[i].id)) { nsCOMPtr tempChannel; - rv = ioService->NewChannel(nsDependentCString(kRedirMap[i].url), - nullptr, nullptr, getter_AddRefs(tempChannel)); + nsCOMPtr tempURI; + rv = NS_NewURI(getter_AddRefs(tempURI), + nsDependentCString(kRedirMap[i].url)); + NS_ENSURE_SUCCESS(rv, rv); + // Bug 1087720 (and Bug 1099296): + // Once all callsites have been updated to call NewChannel2() + // instead of NewChannel() we should have a non-null loadInfo + // consistently. Until then we have to branch on the loadInfo. + if (aLoadInfo) { + rv = NS_NewChannelInternal(getter_AddRefs(tempChannel), + tempURI, + aLoadInfo); + } + else { + rv = ioService->NewChannelFromURI(tempURI, getter_AddRefs(tempChannel)); + } NS_ENSURE_SUCCESS(rv, rv); tempChannel->SetOriginalURI(aURI); diff -Nru thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/components/customizableui/content/panelUI.inc.xul thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/components/customizableui/content/panelUI.inc.xul --- thunderbird-trunk-37.0~a1~hg20141208r17193.218676/mozilla/browser/components/customizableui/content/panelUI.inc.xul 2014-12-08 19:31:02.000000000 +0000 +++ thunderbird-trunk-37.0~a1~hg20141215r17212.219503/mozilla/browser/components/customizableui/content/panelUI.inc.xul 2014-12-15 12:28:55.000000000 +0000 @@ -23,6 +23,7 @@