From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- comm/calendar/base/src/CalRecurrenceInfo.jsm | 847 +++++++++++++++++++++++++++ 1 file changed, 847 insertions(+) create mode 100644 comm/calendar/base/src/CalRecurrenceInfo.jsm (limited to 'comm/calendar/base/src/CalRecurrenceInfo.jsm') diff --git a/comm/calendar/base/src/CalRecurrenceInfo.jsm b/comm/calendar/base/src/CalRecurrenceInfo.jsm new file mode 100644 index 0000000000..f24f210a30 --- /dev/null +++ b/comm/calendar/base/src/CalRecurrenceInfo.jsm @@ -0,0 +1,847 @@ +/* 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/. */ + +var EXPORTED_SYMBOLS = ["CalRecurrenceInfo"]; + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +function getRidKey(date) { + if (!date) { + return null; + } + let timezone = date.timezone; + if (!timezone.isUTC && !timezone.isFloating) { + date = date.getInTimezone(cal.dtz.UTC); + } + return date.icalString; +} + +/** + * Constructor for `calIRecurrenceInfo` objects. + * + * @class + * @implements {calIRecurrenceInfo} + * @param {calIItemBase} [item] - Optional calendar item for which this recurrence applies. + */ +function CalRecurrenceInfo(item) { + this.wrappedJSObject = this; + this.mRecurrenceItems = []; + this.mExceptionMap = {}; + if (item) { + this.item = item; + } +} + +CalRecurrenceInfo.prototype = { + QueryInterface: ChromeUtils.generateQI(["calIRecurrenceInfo"]), + classID: Components.ID("{04027036-5884-4a30-b4af-f2cad79f6edf}"), + + mImmutable: false, + mBaseItem: null, + mEndDate: null, + mRecurrenceItems: null, + mPositiveRules: null, + mNegativeRules: null, + mExceptionMap: null, + + /** + * Helpers + */ + ensureBaseItem() { + if (!this.mBaseItem) { + throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED); + } + }, + ensureMutable() { + if (this.mImmutable) { + throw Components.Exception("", Cr.NS_ERROR_OBJECT_IS_IMMUTABLE); + } + }, + ensureSortedRecurrenceRules() { + if (!this.mPositiveRules || !this.mNegativeRules) { + this.mPositiveRules = []; + this.mNegativeRules = []; + for (let ritem of this.mRecurrenceItems) { + if (ritem.isNegative) { + this.mNegativeRules.push(ritem); + } else { + this.mPositiveRules.push(ritem); + } + } + } + }, + + /** + * Mutability bits + */ + get isMutable() { + return !this.mImmutable; + }, + makeImmutable() { + if (this.mImmutable) { + return; + } + + for (let ritem of this.mRecurrenceItems) { + if (ritem.isMutable) { + ritem.makeImmutable(); + } + } + + for (let ex in this.mExceptionMap) { + let item = this.mExceptionMap[ex]; + if (item.isMutable) { + item.makeImmutable(); + } + } + + this.mImmutable = true; + }, + + clone() { + let cloned = new CalRecurrenceInfo(); + cloned.mBaseItem = this.mBaseItem; + + let clonedItems = []; + for (let ritem of this.mRecurrenceItems) { + clonedItems.push(ritem.clone()); + } + cloned.mRecurrenceItems = clonedItems; + + let clonedExceptions = {}; + for (let exitem in this.mExceptionMap) { + clonedExceptions[exitem] = this.mExceptionMap[exitem].cloneShallow(this.mBaseItem); + } + cloned.mExceptionMap = clonedExceptions; + + return cloned; + }, + + /* + * calIRecurrenceInfo + */ + get item() { + return this.mBaseItem; + }, + set item(value) { + this.ensureMutable(); + + value = cal.unwrapInstance(value); + this.mBaseItem = value; + // patch exception's parentItem: + for (let ex in this.mExceptionMap) { + let exitem = this.mExceptionMap[ex]; + exitem.parentItem = value; + } + }, + + get isFinite() { + this.ensureBaseItem(); + + for (let ritem of this.mRecurrenceItems) { + if (!ritem.isFinite) { + return false; + } + } + return true; + }, + + /** + * Get the item ending date (end date for an event, due date or entry date if available for a task). + * + * @param {calIEvent | calITodo} item - The item. + * @returns {calIDateTime | null} The ending date or null. + */ + getItemEndingDate(item) { + if (item.isEvent()) { + if (item.endDate) { + return item.endDate; + } + } else if (item.isTodo()) { + // Due date must be considered since it is used when displaying the task in agenda view. + if (item.dueDate) { + return item.dueDate; + } else if (item.entryDate) { + return item.entryDate; + } + } + return null; + }, + + get recurrenceEndDate() { + // The lowest and highest possible values of a PRTime (64-bit integer) when in javascript, + // which stores them as floating-point values. + const MIN_PRTIME = -9223372036854775000; + const MAX_PRTIME = 9223372036854775000; + + // If this object is mutable, skip this optimisation, so that we don't have to work out every + // possible modification and invalidate the cached value. Immutable objects are unlikely to + // exist for long enough to really benefit anyway. + if (this.isMutable) { + return MAX_PRTIME; + } + + if (this.mEndDate === null) { + if (this.isFinite) { + this.mEndDate = MIN_PRTIME; + let lastOccurrence = this.getPreviousOccurrence(cal.createDateTime("99991231T235959Z")); + if (lastOccurrence) { + let endingDate = this.getItemEndingDate(lastOccurrence); + if (endingDate) { + this.mEndDate = endingDate.nativeTime; + } + } + + // A modified occurrence may have a new ending date positioned after last occurrence one. + for (let rid in this.mExceptionMap) { + let item = this.mExceptionMap[rid]; + + let endingDate = this.getItemEndingDate(item); + if (endingDate && this.mEndDate < endingDate.nativeTime) { + this.mEndDate = endingDate.nativeTime; + } + } + } else { + this.mEndDate = MAX_PRTIME; + } + } + + return this.mEndDate; + }, + + getRecurrenceItems() { + this.ensureBaseItem(); + + return this.mRecurrenceItems; + }, + + setRecurrenceItems(aItems) { + this.ensureBaseItem(); + this.ensureMutable(); + + // XXX should we clone these? + this.mRecurrenceItems = aItems; + this.mPositiveRules = null; + this.mNegativeRules = null; + }, + + countRecurrenceItems() { + this.ensureBaseItem(); + + return this.mRecurrenceItems.length; + }, + + getRecurrenceItemAt(aIndex) { + this.ensureBaseItem(); + + if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length) { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + return this.mRecurrenceItems[aIndex]; + }, + + appendRecurrenceItem(aItem) { + this.ensureBaseItem(); + this.ensureMutable(); + this.ensureSortedRecurrenceRules(); + + aItem = cal.unwrapInstance(aItem); + this.mRecurrenceItems.push(aItem); + if (aItem.isNegative) { + this.mNegativeRules.push(aItem); + } else { + this.mPositiveRules.push(aItem); + } + }, + + deleteRecurrenceItemAt(aIndex) { + this.ensureBaseItem(); + this.ensureMutable(); + + if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length) { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + if (this.mRecurrenceItems[aIndex].isNegative) { + this.mNegativeRules = null; + } else { + this.mPositiveRules = null; + } + + this.mRecurrenceItems.splice(aIndex, 1); + }, + + deleteRecurrenceItem(aItem) { + aItem = cal.unwrapInstance(aItem); + let pos = this.mRecurrenceItems.indexOf(aItem); + if (pos > -1) { + this.deleteRecurrenceItemAt(pos); + } else { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + }, + + insertRecurrenceItemAt(aItem, aIndex) { + this.ensureBaseItem(); + this.ensureMutable(); + this.ensureSortedRecurrenceRules(); + + if (aIndex < 0 || aIndex > this.mRecurrenceItems.length) { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + aItem = cal.unwrapInstance(aItem); + if (aItem.isNegative) { + this.mNegativeRules.push(aItem); + } else { + this.mPositiveRules.push(aItem); + } + + this.mRecurrenceItems.splice(aIndex, 0, aItem); + }, + + clearRecurrenceItems() { + this.ensureBaseItem(); + this.ensureMutable(); + + this.mRecurrenceItems = []; + this.mPositiveRules = []; + this.mNegativeRules = []; + }, + + /* + * calculations + */ + getNextOccurrence(aTime) { + this.ensureBaseItem(); + this.ensureSortedRecurrenceRules(); + + let startDate = this.mBaseItem.recurrenceStartDate; + let nextOccurrences = []; + let invalidOccurrences; + let negMap = {}; + let minOccRid; + + // Go through all negative rules to create a map of occurrences that + // should be skipped when going through occurrences. + for (let ritem of this.mNegativeRules) { + // TODO Infinite rules (i.e EXRULE) are not taken into account, + // because its very performance hungry and could potentially + // lead to a deadlock (i.e RRULE is canceled out by an EXRULE). + // This is ok for now, since EXRULE is deprecated anyway. + if (ritem.isFinite) { + // Get all occurrences starting at our recurrence start date. + // This is fine, since there will never be an EXDATE that + // occurs before the event started and its illegal to EXDATE an + // RDATE. + let rdates = ritem.getOccurrences(startDate, startDate, null, 0); + // Map all negative dates. + for (let rdate of rdates) { + negMap[getRidKey(rdate)] = true; + } + } else { + cal.WARN( + "Item '" + + this.mBaseItem.title + + "'" + + (this.mBaseItem.calendar ? " (" + this.mBaseItem.calendar.name + ")" : "") + + " has an infinite negative rule (EXRULE)" + ); + } + } + + let bailCounter = 0; + do { + invalidOccurrences = 0; + // Go through all positive rules and get the next recurrence id + // according to that rule. If for all rules the rid is "invalid", + // (i.e an EXDATE removed it, or an exception moved it somewhere + // else), then get the respective next rid. + // + // If in a loop at least one rid is valid (i.e not an exception, not + // an exdate, is after aTime), then remember the lowest one. + for (let i = 0; i < this.mPositiveRules.length; i++) { + let rDateInstance = cal.wrapInstance(this.mPositiveRules[i], Ci.calIRecurrenceDate); + let rRuleInstance = cal.wrapInstance(this.mPositiveRules[i], Ci.calIRecurrenceRule); + if (rDateInstance) { + // RDATEs are special. there is only one date in this rule, + // so no need to search anything. + let rdate = rDateInstance.date; + if (!nextOccurrences[i] && rdate.compare(aTime) > 0) { + // The RDATE falls into range, save it. + nextOccurrences[i] = rdate; + } else { + // The RDATE doesn't fall into range. This rule will + // always be invalid, since it can't give out a date. + nextOccurrences[i] = null; + invalidOccurrences++; + } + } else if (rRuleInstance) { + // RRULEs must not start searching before |startDate|, since + // the pattern is only valid afterwards. If an occurrence + // was found in a previous round, we can go ahead and start + // searching from that occurrence. + let searchStart = nextOccurrences[i] || startDate; + + // Search for the next occurrence after aTime. If the last + // round was invalid, then in this round we need to search + // after nextOccurrences[i] to make sure getNextOccurrence() + // doesn't find the same occurrence again. + let searchDate = + nextOccurrences[i] && nextOccurrences[i].compare(aTime) > 0 + ? nextOccurrences[i] + : aTime; + + nextOccurrences[i] = rRuleInstance.getNextOccurrence(searchStart, searchDate); + } + + // As decided in bug 734245, an EXDATE of type DATE shall also match a DTSTART of type DATE-TIME + let nextKey = getRidKey(nextOccurrences[i]); + let isInExceptionMap = + nextKey && (this.mExceptionMap[nextKey.substring(0, 8)] || this.mExceptionMap[nextKey]); + let isInNegMap = nextKey && (negMap[nextKey.substring(0, 8)] || negMap[nextKey]); + if (nextKey && (isInNegMap || isInExceptionMap)) { + // If the found recurrence id points to either an exception + // (will handle later) or an EXDATE, then nextOccurrences[i] + // is invalid and we might need to try again next round. + invalidOccurrences++; + } else if (nextOccurrences[i]) { + // We have a valid recurrence id (not an exception, not an + // EXDATE, falls into range). We only need to save the + // earliest occurrence after aTime (checking for aTime is + // not needed, since getNextOccurrence() above returns only + // occurrences after aTime). + if (!minOccRid || minOccRid.compare(nextOccurrences[i]) > 0) { + minOccRid = nextOccurrences[i]; + } + } + } + + // To make sure users don't just report bugs like "the application + // hangs", bail out after 100 runs. If this happens, it is most + // likely a bug. + if (bailCounter++ > 100) { + cal.ERROR("Could not find next occurrence after 100 runs!"); + return null; + } + + // We counted how many positive rules found out that their next + // candidate is invalid. If all rules produce invalid next + // occurrences, a second round is needed. + } while (invalidOccurrences == this.mPositiveRules.length); + + // Since we need to compare occurrences by date, save the rid found + // above also as a date. This works out because above we skipped + // exceptions. + let minOccDate = minOccRid; + + // Scan exceptions for any dates earlier than the above found + // minOccDate, but still after aTime. + for (let ex in this.mExceptionMap) { + let exc = this.mExceptionMap[ex]; + let start = exc.recurrenceStartDate; + if (start.compare(aTime) > 0 && (!minOccDate || start.compare(minOccDate) <= 0)) { + // This exception is earlier, save its rid (for getting the + // occurrence later on) and its date (for comparing to other + // exceptions). + minOccRid = exc.recurrenceId; + minOccDate = start; + } + } + + // If we found a recurrence id any time above, then return the + // occurrence for it. + return minOccRid ? this.getOccurrenceFor(minOccRid) : null; + }, + + getPreviousOccurrence(aTime) { + // HACK We never know how early an RDATE might be before the actual + // recurrence start. Since rangeStart cannot be null for recurrence + // items like calIRecurrenceRule, we need to work around by supplying a + // very early date. Again, this might have a high performance penalty. + let early = cal.createDateTime(); + early.icalString = "00000101T000000Z"; + + let rids = this.calculateDates(early, aTime, 0); + // The returned dates are sorted, so the last one is a good + // candidate, if it exists. + return rids.length > 0 ? this.getOccurrenceFor(rids[rids.length - 1].id) : null; + }, + + // internal helper function; + calculateDates(aRangeStart, aRangeEnd, aMaxCount) { + this.ensureBaseItem(); + this.ensureSortedRecurrenceRules(); + + // workaround for UTC- timezones + let rangeStart = cal.dtz.ensureDateTime(aRangeStart); + let rangeEnd = cal.dtz.ensureDateTime(aRangeEnd); + + // If aRangeStart falls in the middle of an occurrence, libical will + // not return that occurrence when we go and ask for an + // icalrecur_iterator_new. This actually seems fairly rational, so + // instead of hacking libical, I'm going to move aRangeStart back far + // enough to make sure we get the occurrences we might miss. + let searchStart = rangeStart.clone(); + let baseDuration = this.mBaseItem.duration; + if (baseDuration) { + let duration = baseDuration.clone(); + duration.isNegative = true; + searchStart.addDuration(duration); + } + + let startDate = this.mBaseItem.recurrenceStartDate; + if (startDate == null) { + // Todo created by other apps may have a saved recurrence but + // start and due dates disabled. Since no recurrenceStartDate, + // treat as undated task. + return []; + } + + let dates = []; + + // toss in exceptions first. Save a map of all exceptions ids, so we + // don't add the wrong occurrences later on. + let occurrenceMap = {}; + for (let ex in this.mExceptionMap) { + let item = this.mExceptionMap[ex]; + let occDate = cal.item.checkIfInRange(item, aRangeStart, aRangeEnd, true); + occurrenceMap[ex] = true; + if (occDate) { + dates.push({ id: item.recurrenceId, rstart: occDate }); + } + } + + // DTSTART/DUE is always part of the (positive) expanded set: + // DTSTART always equals RECURRENCE-ID for items expanded from RRULE + let baseOccDate = cal.item.checkIfInRange(this.mBaseItem, aRangeStart, aRangeEnd, true); + let baseOccDateKey = getRidKey(baseOccDate); + if (baseOccDate && !occurrenceMap[baseOccDateKey]) { + occurrenceMap[baseOccDateKey] = true; + dates.push({ id: baseOccDate, rstart: baseOccDate }); + } + + // if both range start and end are specified, we ask for all of the occurrences, + // to make sure we catch all possible exceptions. If aRangeEnd isn't specified, + // then we have to ask for aMaxCount, and hope for the best. + let maxCount; + if (rangeStart && rangeEnd) { + maxCount = 0; + } else { + maxCount = aMaxCount; + } + + // Apply positive rules + for (let ritem of this.mPositiveRules) { + let cur_dates = ritem.getOccurrences(startDate, searchStart, rangeEnd, maxCount); + if (cur_dates.length == 0) { + continue; + } + + // if positive, we just add these date to the existing set, + // but only if they're not already there + + let index = 0; + let len = cur_dates.length; + + // skip items before rangeStart due to searchStart libical hack: + if (rangeStart && baseDuration) { + for (; index < len; ++index) { + let date = cur_dates[index].clone(); + date.addDuration(baseDuration); + if (rangeStart.compare(date) < 0) { + break; + } + } + } + for (; index < len; ++index) { + let date = cur_dates[index]; + let dateKey = getRidKey(date); + if (occurrenceMap[dateKey]) { + // Don't add occurrences twice (i.e exception was + // already added before) + continue; + } + dates.push({ id: date, rstart: date }); + occurrenceMap[dateKey] = true; + } + } + + dates.sort((a, b) => a.rstart.compare(b.rstart)); + + // Apply negative rules + for (let ritem of this.mNegativeRules) { + let cur_dates = ritem.getOccurrences(startDate, searchStart, rangeEnd, maxCount); + if (cur_dates.length == 0) { + continue; + } + + // XXX: i'm pretty sure negative dates can't really have exceptions + // (like, you can't make a date "real" by defining an RECURRENCE-ID which + // is an EXDATE, and then giving it a real DTSTART) -- so we don't + // check exceptions here + for (let dateToRemove of cur_dates) { + let dateToRemoveKey = getRidKey(dateToRemove); + if (dateToRemove.isDate) { + // As decided in bug 734245, an EXDATE of type DATE shall also match a DTSTART of type DATE-TIME + let toRemove = []; + for (let occurenceKey in occurrenceMap) { + if (occurrenceMap[occurenceKey] && occurenceKey.substring(0, 8) == dateToRemoveKey) { + dates = dates.filter(date => date.id.compare(dateToRemove) != 0); + toRemove.push(occurenceKey); + } + } + for (let i = 0; i < toRemove.length; i++) { + delete occurrenceMap[toRemove[i]]; + } + } else if (occurrenceMap[dateToRemoveKey]) { + // TODO PERF Theoretically we could use occurrence map + // to construct the array of occurrences. Right now I'm + // just using the occurrence map to skip the filter + // action if the occurrence isn't there anyway. + dates = dates.filter(date => date.id.compare(dateToRemove) != 0); + delete occurrenceMap[dateToRemoveKey]; + } + } + } + + // The list was already sorted above, chop anything over aMaxCount, if + // specified. + if (aMaxCount && dates.length > aMaxCount) { + dates = dates.slice(0, aMaxCount); + } + + return dates; + }, + + getOccurrenceDates(aRangeStart, aRangeEnd, aMaxCount) { + let dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount); + dates = dates.map(date => date.rstart); + return dates; + }, + + getOccurrences(aRangeStart, aRangeEnd, aMaxCount) { + let results = []; + let dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount); + if (dates.length) { + let count; + if (aMaxCount) { + count = Math.min(aMaxCount, dates.length); + } else { + count = dates.length; + } + + for (let i = 0; i < count; i++) { + results.push(this.getOccurrenceFor(dates[i].id)); + } + } + return results; + }, + + getOccurrenceFor(aRecurrenceId) { + let proxy = this.getExceptionFor(aRecurrenceId); + if (!proxy) { + return this.item.createProxy(aRecurrenceId); + } + return proxy; + }, + + removeOccurrenceAt(aRecurrenceId) { + this.ensureBaseItem(); + this.ensureMutable(); + + let rdate = cal.createRecurrenceDate(); + rdate.isNegative = true; + rdate.date = aRecurrenceId.clone(); + + this.removeExceptionFor(rdate.date); + + this.appendRecurrenceItem(rdate); + }, + + restoreOccurrenceAt(aRecurrenceId) { + this.ensureBaseItem(); + this.ensureMutable(); + this.ensureSortedRecurrenceRules(); + + for (let i = 0; i < this.mRecurrenceItems.length; i++) { + let rdate = cal.wrapInstance(this.mRecurrenceItems[i], Ci.calIRecurrenceDate); + if (rdate) { + if (rdate.isNegative && rdate.date.compare(aRecurrenceId) == 0) { + return this.deleteRecurrenceItemAt(i); + } + } + } + + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + }, + + // + // exceptions + // + + // + // Some notes: + // + // The way I read ICAL, RECURRENCE-ID is used to specify a + // particular instance of a recurring event, according to the + // RRULEs/RDATEs/etc. specified in the base event. If one of + // these is to be changed ("an exception"), then it can be + // referenced via the UID of the original event, and a + // RECURRENCE-ID of the start time of the instance to change. + // This, to me, means that an event where one of the instances has + // changed to a different time has a RECURRENCE-ID of the original + // start time, and a DTSTART/DTEND representing the new time. + // + // ITIP, however, seems to want something different -- you're + // supposed to use UID/RECURRENCE-ID to select from the current + // set of occurrences of an event. If you change the DTSTART for + // an instance, you're supposed to use the old (original) DTSTART + // as the RECURRENCE-ID, and put the new time as the DTSTART. + // However, after that change, to refer to that instance in the + // future, you have to use the modified DTSTART as the + // RECURRENCE-ID. This madness is described in ITIP end of + // section 3.7.1. + // + // This implementation does the first approach (RECURRENCE-ID will + // never change even if DTSTART for that instance changes), which + // I think is the right thing to do for CalDAV; I don't know what + // we'll do for incoming ITIP events though. + // + modifyException(anItem, aTakeOverOwnership) { + this.ensureBaseItem(); + + anItem = cal.unwrapInstance(anItem); + + if ( + anItem.parentItem.calendar != this.mBaseItem.calendar && + anItem.parentItem.id != this.mBaseItem.id + ) { + cal.ERROR("recurrenceInfo::addException: item parentItem != this.mBaseItem (calendar/id)!"); + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + if (anItem.recurrenceId == null) { + cal.ERROR("recurrenceInfo::addException: item with null recurrenceId!"); + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + let itemtoadd; + if (aTakeOverOwnership && anItem.isMutable) { + itemtoadd = anItem; + itemtoadd.parentItem = this.mBaseItem; + } else { + itemtoadd = anItem.cloneShallow(this.mBaseItem); + } + + // we're going to assume that the recurrenceId is valid here, + // because presumably the item came from one of our functions + + let exKey = getRidKey(itemtoadd.recurrenceId); + this.mExceptionMap[exKey] = itemtoadd; + }, + + getExceptionFor(aRecurrenceId) { + this.ensureBaseItem(); + // Interface calIRecurrenceInfo specifies result be null if not found. + // To avoid strict "reference to undefined property" warning, appending + // "|| null" gives explicit result in case where property undefined + // (or false, 0, null, or "", but here it should never be those values). + return this.mExceptionMap[getRidKey(aRecurrenceId)] || null; + }, + + removeExceptionFor(aRecurrenceId) { + this.ensureBaseItem(); + delete this.mExceptionMap[getRidKey(aRecurrenceId)]; + }, + + getExceptionIds() { + this.ensureBaseItem(); + + let ids = []; + for (let ex in this.mExceptionMap) { + let item = this.mExceptionMap[ex]; + ids.push(item.recurrenceId); + } + return ids; + }, + + // changing the startdate of an item needs to take exceptions into account. + // in case we're about to modify a parentItem (aka 'folded' item), we need + // to modify the recurrenceId's of all possibly existing exceptions as well. + onStartDateChange(aNewStartTime, aOldStartTime) { + // passing null for the new starttime would indicate an error condition, + // since having a recurrence without a starttime is invalid. + cal.ASSERT(aNewStartTime, "invalid arg!", true); + + // no need to check for changes if there's no previous starttime. + if (!aOldStartTime) { + return; + } + + // convert both dates to UTC since subtractDate is not timezone aware. + let timeDiff = aNewStartTime + .getInTimezone(cal.dtz.UTC) + .subtractDate(aOldStartTime.getInTimezone(cal.dtz.UTC)); + + let rdates = {}; + + // take RDATE's and EXDATE's into account. + const kCalIRecurrenceDate = Ci.calIRecurrenceDate; + let ritems = this.getRecurrenceItems(); + for (let ritem of ritems) { + let rDateInstance = cal.wrapInstance(ritem, kCalIRecurrenceDate); + let rRuleInstance = cal.wrapInstance(ritem, Ci.calIRecurrenceRule); + if (rDateInstance) { + ritem = rDateInstance; + let date = ritem.date; + date.addDuration(timeDiff); + if (!ritem.isNegative) { + rdates[getRidKey(date)] = date; + } + ritem.date = date; + } else if (rRuleInstance) { + ritem = rRuleInstance; + if (!ritem.isByCount) { + let untilDate = ritem.untilDate; + if (untilDate) { + untilDate.addDuration(timeDiff); + ritem.untilDate = untilDate; + } + } + } + } + + let startTimezone = aNewStartTime.timezone; + let modifiedExceptions = []; + for (let exid of this.getExceptionIds()) { + let ex = this.getExceptionFor(exid); + if (ex) { + ex = ex.clone(); + // track RECURRENCE-IDs in DTSTART's or RDATE's timezone, + // otherwise those won't match any longer w.r.t DST: + let rid = ex.recurrenceId; + let rdate = rdates[getRidKey(rid)]; + rid = rid.getInTimezone(rdate ? rdate.timezone : startTimezone); + rid.addDuration(timeDiff); + ex.recurrenceId = rid; + cal.item.shiftOffset(ex, timeDiff); + modifiedExceptions.push(ex); + this.removeExceptionFor(exid); + } + } + for (let modifiedEx of modifiedExceptions) { + this.modifyException(modifiedEx, true); + } + }, + + onIdChange(aNewId) { + // patch all overridden items' id: + for (let ex in this.mExceptionMap) { + let item = this.mExceptionMap[ex]; + item.id = aNewId; + } + }, +}; -- cgit v1.2.3