1 /* -*- Mode: javascript; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 * ***** BEGIN LICENSE BLOCK *****
3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 *
5 * The contents of this file are subject to the Mozilla Public License Version
6 * 1.1 (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 * http://www.mozilla.org/MPL/
9 *
10 * Software distributed under the License is distributed on an "AS IS" basis,
11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 * for the specific language governing rights and limitations under the
13 * License.
14 *
15 * The Original Code is Mozilla Calendar code.
16 *
17 * The Initial Developer of the Original Code is
18 * Michiel van Leeuwen <mvl@exedo.nl>.
19 * Portions created by the Initial Developer are Copyright (C) 2006
20 * the Initial Developer. All Rights Reserved.
21 *
22 * Contributor(s):
23 *
24 * Alternatively, the contents of this file may be used under the terms of
25 * either the GNU General Public License Version 2 or later (the "GPL"), or
26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27 * in which case the provisions of the GPL or the LGPL are applicable instead
28 * of those above. If you wish to allow use of your version of this file only
29 * under the terms of either the GPL or the LGPL, and not to allow others to
30 * use your version of this file under the terms of the MPL, indicate your
31 * decision by deleting the provisions above and replace them with the notice
32 * and other provisions required by the GPL or the LGPL. If you do not delete
33 * the provisions above, a recipient may use your version of this file under
34 * the terms of any one of the MPL, the GPL or the LGPL.
35 *
36 * ***** END LICENSE BLOCK ***** */
37
calIcsParser
38 function calIcsParser() {
39 this.wrappedJSObject = this;
40 this.mItems = new Array();
41 this.mParentlessItems = new Array();
42 this.mComponents = new Array();
43 this.mProperties = new Array();
44 }
45
46 calIcsParser.prototype.QueryInterface =
QueryInterface
47 function QueryInterface(aIID) {
48 if (!aIID.equals(Components.interfaces.nsISupports) &&
49 !aIID.equals(Components.interfaces.calIIcsParser)) {
50 throw Components.results.NS_ERROR_NO_INTERFACE;
51 }
52
53 return this;
54 };
55
56 calIcsParser.prototype.parseString =
ip_parseString
57 function ip_parseString(aICSString, aTzProvider) {
58 var rootComp = getIcsService().parseICS(aICSString, aTzProvider);
59 var calComp;
60 // libical returns the vcalendar component if there is just one vcalendar.
61 // If there are multiple vcalendars, it returns an xroot component, with
62 // those vcalendar children. We need to handle both.
63 if (rootComp.componentType == 'VCALENDAR') {
64 calComp = rootComp;
65 } else {
66 calComp = rootComp.getFirstSubcomponent('VCALENDAR');
67 }
68
69 var unexpandedItems = [];
70 var uid2parent = {};
71 var excItems = [];
72
73 while (calComp) {
74
75 // Get unknown properties
76 var prop = calComp.getFirstProperty("ANY");
77 while (prop) {
78 if (prop.propertyName != "VERSION" &&
79 prop.propertyName != "PRODID") {
80 this.mProperties.push(prop);
81 }
82 prop = calComp.getNextProperty("ANY");
83 }
84
85 var prodId = calComp.getFirstProperty("PRODID");
86 var isFromOldSunbird;
87 if (prodId) {
88 isFromOldSunbird = prodId.value == "-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN";
89 }
90
91 var subComp = calComp.getFirstSubcomponent("ANY");
92 while (subComp) {
93 var item = null;
94 switch (subComp.componentType) {
95 case "VEVENT":
96 item = Components.classes["@mozilla.org/calendar/event;1"]
97 .createInstance(Components.interfaces.calIEvent);
98 break;
99 case "VTODO":
100 item = Components.classes["@mozilla.org/calendar/todo;1"]
101 .createInstance(Components.interfaces.calITodo);
102 break;
103 case "VTIMEZONE":
104 // this should already be attached to the relevant
105 // events in the calendar, so there's no need to
106 // do anything with it here.
107 break;
108 default:
109 this.mComponents.push(subComp);
110 }
111
112 if (item) {
113 item.icalComponent = subComp;
114
115 // Only try to fix ICS from Sunbird 0.2 (and earlier) if it
116 // has an EXDATE.
117 hasExdate = subComp.getFirstProperty("EXDATE");
118 if (isFromOldSunbird && hasExdate) {
119 item = fixOldSunbirdExceptions(item);
120 }
121
122 var rid = item.recurrenceId;
123 if (!rid) {
124 unexpandedItems.push(item);
125 if (item.recurrenceInfo) {
126 uid2parent[item.id] = item;
127 }
128 } else {
129 // force no recurrence info:
130 item.recurrenceInfo = null;
131 excItems.push(item);
132 }
133 }
134 subComp = calComp.getNextSubcomponent("ANY");
135 }
136 calComp = rootComp.getNextSubcomponent("VCALENDAR");
137 }
138
139 // tag "exceptions", i.e. items with rid:
140 for each (var item in excItems) {
141 var parent = uid2parent[item.id];
142 if (parent) {
143 item.parentItem = parent;
144 parent.recurrenceInfo.modifyException(item);
145 } else { // a parentless one
146 this.mParentlessItems.push(item);
147 }
148 }
149
150 for each (var item in unexpandedItems) {
151 this.mItems.push(item);
152 }
153 };
154
155 calIcsParser.prototype.parseFromStream =
ip_parseFromStream
156 function ip_parseFromStream(aStream, aTzProvider) {
157 // Read in the string. Note that it isn't a real string at this point,
158 // because likely, the file is utf8. The multibyte chars show up as multiple
159 // 'chars' in this string. So call it an array of octets for now.
160
161 var octetArray = [];
162 var binaryIS = Components.classes["@mozilla.org/binaryinputstream;1"]
163 .createInstance(Components.interfaces.nsIBinaryInputStream);
164 binaryIS.setInputStream(aStream);
165 octetArray = binaryIS.readByteArray(binaryIS.available());
166
167
168 // Some other apps (most notably, sunbird 0.2) happily splits an UTF8
169 // character between the octets, and adds a newline and space between them,
170 // for ICS folding. Unfold manually before parsing the file as utf8.This is
171 // UTF8 safe, because octets with the first bit 0 are always one-octet
172 // characters. So the space or the newline never can be part of a multi-byte
173 // char.
174 for (var i = octetArray.length - 2; i >= 0; i--) {
175 if (octetArray[i] == "\n" && octetArray[i+1] == " ") {
176 octetArray = octetArray.splice(i, 2);
177 }
178 }
179
180 // Interpret the byte-array as a UTF8-string, and convert into a
181 // javascript string.
182 var unicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
183 .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
184 // ICS files are always UTF8
185 unicodeConverter.charset = "UTF-8";
186 var str = unicodeConverter.convertFromByteArray(octetArray, octetArray.length);
187 return this.parseString(str, aTzProvider);
188 }
189
190 calIcsParser.prototype.getItems =
ip_getItems
191 function ip_getItems(aCount) {
192 aCount.value = this.mItems.length;
193 return this.mItems.concat([]); //clone
194 }
195
196 calIcsParser.prototype.getParentlessItems =
ip_getParentlessItems
197 function ip_getParentlessItems(aCount) {
198 aCount.value = this.mParentlessItems.length;
199 return this.mParentlessItems.concat([]); //clone
200 }
201
202 calIcsParser.prototype.getProperties =
ip_getProperties
203 function ip_getProperties(aCount) {
204 aCount.value = this.mProperties.length;
205 return this.mProperties.concat([]); //clone
206 }
207
208 calIcsParser.prototype.getComponents =
ip_getComponents
209 function ip_getComponents(aCount) {
210 aCount.value = this.mComponents.length;
211 return this.mComponents.concat([]); //clone
212 }
213
214 // Helper function to deal with the busted exdates from Sunbird 0.2
215 // When Sunbird 0.2 (and earlier) creates EXDATEs, they are set to
216 // 00:00:00 floating rather than to the item's DTSTART. This fixes that.
217 // (bug 354073)
fixOldSunbirdExceptions
218 function fixOldSunbirdExceptions(aItem) {
219 const kCalIRecurrenceDate = Components.interfaces.calIRecurrenceDate;
220
221 var item = aItem;
222 var ritems = aItem.recurrenceInfo.getRecurrenceItems({});
223 for each (var ritem in ritems) {
224 // EXDATEs are represented as calIRecurrenceDates, which are
225 // negative and finite.
226 if (ritem instanceof kCalIRecurrenceDate &&
227 ritem.isNegative &&
228 ritem.isFinite) {
229 // Only mess with the exception if its time is wrong.
230 var oldDate = aItem.startDate || aItem.entryDate;
231 if (ritem.date.compare(oldDate) != 0) {
232 var newRitem = ritem.clone();
233 // All we want from aItem is the time and timezone.
234 newRitem.date.timezone = oldDate.timezone;
235 newRitem.date.hour = oldDate.hour;
236 newRitem.date.minute = oldDate.minute;
237 newRitem.date.second = oldDate.second;
238 item.recurrenceInfo.appendRecurrenceItem(newRitem);
239 item.recurrenceInfo.deleteRecurrenceItem(ritem);
240 }
241 }
242 }
243 return item;
244 }