1 | /* |
2 | This file is part of the kcalcore library. |
3 | |
4 | Copyright (c) 2005-2007 David Jarvie <djarvie@kde.org> |
5 | |
6 | This library is free software; you can redistribute it and/or |
7 | modify it under the terms of the GNU Library General Public |
8 | License as published by the Free Software Foundation; either |
9 | version 2 of the License, or (at your option) any later version. |
10 | |
11 | This library is distributed in the hope that it will be useful, |
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
14 | Library General Public License for more details. |
15 | |
16 | You should have received a copy of the GNU Library General Public License |
17 | along with this library; see the file COPYING.LIB. If not, write to |
18 | the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
19 | Boston, MA 02110-1301, USA. |
20 | */ |
21 | #include <config-kcalcore.h> |
22 | |
23 | #include "icaltimezones.h" |
24 | #include "icalformat.h" |
25 | #include "icalformat_p.h" |
26 | #include "recurrence.h" |
27 | #include "recurrencerule.h" |
28 | |
29 | #include <KDebug> |
30 | #include <KDateTime> |
31 | #include <KSystemTimeZone> |
32 | |
33 | #include <QtCore/QDateTime> |
34 | #include <QtCore/QFile> |
35 | #include <QtCore/QTextStream> |
36 | |
37 | extern "C" { |
38 | #include <libical/ical.h> |
39 | #include <icaltimezone.h> |
40 | } |
41 | |
42 | #if defined(HAVE_UUID_UUID_H) |
43 | #include <uuid/uuid.h> |
44 | #endif |
45 | |
46 | using namespace KCalCore; |
47 | |
48 | // Minimum repetition counts for VTIMEZONE RRULEs |
49 | static const int minRuleCount = 5; // for any RRULE |
50 | static const int minPhaseCount = 8; // for separate STANDARD/DAYLIGHT component |
51 | |
52 | // Convert an ical time to QDateTime, preserving the UTC indicator |
53 | static QDateTime toQDateTime(const icaltimetype &t) |
54 | { |
55 | return QDateTime(QDate(t.year, t.month, t.day), |
56 | QTime(t.hour, t.minute, t.second), |
57 | (t.is_utc ? Qt::UTC : Qt::LocalTime)); |
58 | } |
59 | |
60 | // Maximum date for time zone data. |
61 | // It's not sensible to try to predict them very far in advance, because |
62 | // they can easily change. Plus, it limits the processing required. |
63 | static QDateTime MAX_DATE() |
64 | { |
65 | static QDateTime dt; |
66 | if (!dt.isValid()) { |
67 | dt = QDateTime(QDate::currentDate().addYears(20), QTime(0, 0, 0)); |
68 | } |
69 | return dt; |
70 | } |
71 | |
72 | static icaltimetype writeLocalICalDateTime(const QDateTime &utc, int offset) |
73 | { |
74 | const QDateTime local = utc.addSecs(offset); |
75 | icaltimetype t = icaltime_null_time(); |
76 | t.year = local.date().year(); |
77 | t.month = local.date().month(); |
78 | t.day = local.date().day(); |
79 | t.hour = local.time().hour(); |
80 | t.minute = local.time().minute(); |
81 | t.second = local.time().second(); |
82 | t.is_date = 0; |
83 | t.zone = 0; |
84 | t.is_utc = 0; |
85 | return t; |
86 | } |
87 | |
88 | namespace KCalCore { |
89 | |
90 | /******************************************************************************/ |
91 | |
92 | //@cond PRIVATE |
93 | class ICalTimeZonesPrivate |
94 | { |
95 | public: |
96 | ICalTimeZonesPrivate() {} |
97 | ICalTimeZones::ZoneMap zones; |
98 | }; |
99 | //@endcond |
100 | |
101 | ICalTimeZones::ICalTimeZones() |
102 | : d(new ICalTimeZonesPrivate) |
103 | { |
104 | } |
105 | |
106 | ICalTimeZones::ICalTimeZones(const ICalTimeZones &rhs) |
107 | : d(new ICalTimeZonesPrivate()) |
108 | { |
109 | d->zones = rhs.d->zones; |
110 | } |
111 | |
112 | ICalTimeZones &ICalTimeZones::operator=(const ICalTimeZones &rhs) |
113 | { |
114 | // check for self assignment |
115 | if (&rhs == this) { |
116 | return *this; |
117 | } |
118 | d->zones = rhs.d->zones; |
119 | return *this; |
120 | } |
121 | |
122 | ICalTimeZones::~ICalTimeZones() |
123 | { |
124 | delete d; |
125 | } |
126 | |
127 | const ICalTimeZones::ZoneMap ICalTimeZones::zones() const |
128 | { |
129 | return d->zones; |
130 | } |
131 | |
132 | bool ICalTimeZones::add(const ICalTimeZone &zone) |
133 | { |
134 | if (!zone.isValid()) { |
135 | return false; |
136 | } |
137 | if (d->zones.find(zone.name()) != d->zones.end()) { |
138 | return false; // name already exists |
139 | } |
140 | |
141 | d->zones.insert(zone.name(), zone); |
142 | return true; |
143 | } |
144 | |
145 | ICalTimeZone ICalTimeZones::remove(const ICalTimeZone &zone) |
146 | { |
147 | if (zone.isValid()) { |
148 | for (ZoneMap::Iterator it = d->zones.begin(), end = d->zones.end(); it != end; ++it) { |
149 | if (it.value() == zone) { |
150 | d->zones.erase(it); |
151 | return (zone == ICalTimeZone::utc()) ? ICalTimeZone() : zone; |
152 | } |
153 | } |
154 | } |
155 | return ICalTimeZone(); |
156 | } |
157 | |
158 | ICalTimeZone ICalTimeZones::remove(const QString &name) |
159 | { |
160 | if (!name.isEmpty()) { |
161 | ZoneMap::Iterator it = d->zones.find(name); |
162 | if (it != d->zones.end()) { |
163 | const ICalTimeZone zone = it.value(); |
164 | d->zones.erase(it); |
165 | return (zone == ICalTimeZone::utc()) ? ICalTimeZone() : zone; |
166 | } |
167 | } |
168 | return ICalTimeZone(); |
169 | } |
170 | |
171 | void ICalTimeZones::clear() |
172 | { |
173 | d->zones.clear(); |
174 | } |
175 | |
176 | int ICalTimeZones::count() |
177 | { |
178 | return d->zones.count(); |
179 | } |
180 | |
181 | ICalTimeZone ICalTimeZones::zone(const QString &name) const |
182 | { |
183 | if (!name.isEmpty()) { |
184 | ZoneMap::ConstIterator it = d->zones.constFind(name); |
185 | if (it != d->zones.constEnd()) { |
186 | return it.value(); |
187 | } |
188 | } |
189 | return ICalTimeZone(); // error |
190 | } |
191 | |
192 | ICalTimeZone ICalTimeZones::zone(const ICalTimeZone &zone) const |
193 | { |
194 | if (zone.isValid()) { |
195 | QMapIterator<QString, ICalTimeZone> it(d->zones); |
196 | while (it.hasNext()) { |
197 | it.next(); |
198 | const ICalTimeZone tz = it.value(); |
199 | const QList<KTimeZone::Transition> list1 = tz.transitions(); |
200 | const QList<KTimeZone::Transition> list2 = zone.transitions(); |
201 | if (list1.size() == list2.size()) { |
202 | int i = 0; |
203 | int matches = 0; |
204 | for (; i < list1.size(); ++i) { |
205 | const KTimeZone::Transition t1 = list1[ i ]; |
206 | const KTimeZone::Transition t2 = list2[ i ]; |
207 | if ((t1.time() == t2.time()) && |
208 | (t1.phase().utcOffset() == t2.phase().utcOffset()) && |
209 | (t1.phase().isDst() == t2.phase().isDst())) { |
210 | matches++; |
211 | } |
212 | } |
213 | if (matches == i) { |
214 | // Existing zone has all the transitions of the given zone. |
215 | return tz; |
216 | } |
217 | } |
218 | } |
219 | } |
220 | return ICalTimeZone(); // not found |
221 | } |
222 | |
223 | /******************************************************************************/ |
224 | |
225 | ICalTimeZoneBackend::ICalTimeZoneBackend() |
226 | : KTimeZoneBackend() |
227 | {} |
228 | |
229 | ICalTimeZoneBackend::ICalTimeZoneBackend(ICalTimeZoneSource *source, |
230 | const QString &name, |
231 | const QString &countryCode, |
232 | float latitude, float longitude, |
233 | const QString &) |
234 | : KTimeZoneBackend(source, name, countryCode, latitude, longitude, comment) |
235 | {} |
236 | |
237 | ICalTimeZoneBackend::ICalTimeZoneBackend(const KTimeZone &tz, const QDate &earliest) |
238 | : KTimeZoneBackend(0, tz.name(), tz.countryCode(), tz.latitude(), tz.longitude(), tz.comment()) |
239 | { |
240 | Q_UNUSED(earliest); |
241 | } |
242 | |
243 | ICalTimeZoneBackend::~ICalTimeZoneBackend() |
244 | {} |
245 | |
246 | KTimeZoneBackend *ICalTimeZoneBackend::clone() const |
247 | { |
248 | return new ICalTimeZoneBackend(*this); |
249 | } |
250 | |
251 | QByteArray ICalTimeZoneBackend::type() const |
252 | { |
253 | return "ICalTimeZone" ; |
254 | } |
255 | |
256 | bool ICalTimeZoneBackend::hasTransitions(const KTimeZone *caller) const |
257 | { |
258 | Q_UNUSED(caller); |
259 | return true; |
260 | } |
261 | |
262 | void ICalTimeZoneBackend::virtual_hook(int id, void *data) |
263 | { |
264 | Q_UNUSED(id); |
265 | Q_UNUSED(data); |
266 | } |
267 | |
268 | /******************************************************************************/ |
269 | |
270 | ICalTimeZone::ICalTimeZone() |
271 | : KTimeZone(new ICalTimeZoneBackend()) |
272 | {} |
273 | |
274 | ICalTimeZone::ICalTimeZone(ICalTimeZoneSource *source, const QString &name, |
275 | ICalTimeZoneData *data) |
276 | : KTimeZone(new ICalTimeZoneBackend(source, name)) |
277 | { |
278 | setData(data); |
279 | } |
280 | |
281 | ICalTimeZone::ICalTimeZone(const KTimeZone &tz, const QDate &earliest) |
282 | : KTimeZone(new ICalTimeZoneBackend(0, tz.name(), tz.countryCode(), |
283 | tz.latitude(), tz.longitude(), |
284 | tz.comment())) |
285 | { |
286 | const KTimeZoneData *data = tz.data(true); |
287 | if (data) { |
288 | const ICalTimeZoneData *icaldata = dynamic_cast<const ICalTimeZoneData*>(data); |
289 | if (icaldata) { |
290 | setData(new ICalTimeZoneData(*icaldata)); |
291 | } else { |
292 | setData(new ICalTimeZoneData(*data, tz, earliest)); |
293 | } |
294 | } |
295 | } |
296 | |
297 | ICalTimeZone::~ICalTimeZone() |
298 | {} |
299 | |
300 | QString ICalTimeZone::city() const |
301 | { |
302 | const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>(data()); |
303 | return dat ? dat->city() : QString(); |
304 | } |
305 | |
306 | QByteArray ICalTimeZone::url() const |
307 | { |
308 | const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>(data()); |
309 | return dat ? dat->url() : QByteArray(); |
310 | } |
311 | |
312 | QDateTime ICalTimeZone::lastModified() const |
313 | { |
314 | const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>(data()); |
315 | return dat ? dat->lastModified() : QDateTime(); |
316 | } |
317 | |
318 | QByteArray ICalTimeZone::vtimezone() const |
319 | { |
320 | const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>(data()); |
321 | return dat ? dat->vtimezone() : QByteArray(); |
322 | } |
323 | |
324 | icaltimezone *ICalTimeZone::icalTimezone() const |
325 | { |
326 | const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>(data()); |
327 | return dat ? dat->icalTimezone() : 0; |
328 | } |
329 | |
330 | bool ICalTimeZone::update(const ICalTimeZone &other) |
331 | { |
332 | if (!updateBase(other)) { |
333 | return false; |
334 | } |
335 | |
336 | KTimeZoneData *otherData = other.data() ? other.data()->clone() : 0; |
337 | setData(otherData, other.source()); |
338 | return true; |
339 | } |
340 | |
341 | ICalTimeZone ICalTimeZone::utc() |
342 | { |
343 | static ICalTimeZone utcZone; |
344 | if (!utcZone.isValid()) { |
345 | ICalTimeZoneSource tzs; |
346 | utcZone = tzs.parse(icaltimezone_get_utc_timezone()); |
347 | } |
348 | return utcZone; |
349 | } |
350 | |
351 | void ICalTimeZone::virtual_hook(int id, void *data) |
352 | { |
353 | Q_UNUSED(id); |
354 | Q_UNUSED(data); |
355 | } |
356 | /******************************************************************************/ |
357 | |
358 | //@cond PRIVATE |
359 | class ICalTimeZoneDataPrivate |
360 | { |
361 | public: |
362 | ICalTimeZoneDataPrivate() : icalComponent(0) {} |
363 | |
364 | ~ICalTimeZoneDataPrivate() |
365 | { |
366 | if (icalComponent) { |
367 | icalcomponent_free(icalComponent); |
368 | } |
369 | } |
370 | |
371 | icalcomponent *component() const { |
372 | return icalComponent; |
373 | } |
374 | void setComponent(icalcomponent *c) |
375 | { |
376 | if (icalComponent) { |
377 | icalcomponent_free(icalComponent); |
378 | } |
379 | icalComponent = c; |
380 | } |
381 | |
382 | QString location; // name of city for this time zone |
383 | QByteArray url; // URL of published VTIMEZONE definition (optional) |
384 | QDateTime lastModified; // time of last modification of the VTIMEZONE component (optional) |
385 | |
386 | private: |
387 | icalcomponent *icalComponent; // ical component representing this time zone |
388 | }; |
389 | //@endcond |
390 | |
391 | ICalTimeZoneData::ICalTimeZoneData() |
392 | : d(new ICalTimeZoneDataPrivate()) |
393 | { |
394 | } |
395 | |
396 | ICalTimeZoneData::ICalTimeZoneData(const ICalTimeZoneData &rhs) |
397 | : KTimeZoneData(rhs), |
398 | d(new ICalTimeZoneDataPrivate()) |
399 | { |
400 | d->location = rhs.d->location; |
401 | d->url = rhs.d->url; |
402 | d->lastModified = rhs.d->lastModified; |
403 | d->setComponent(icalcomponent_new_clone(rhs.d->component())); |
404 | } |
405 | |
406 | ICalTimeZoneData::ICalTimeZoneData(const KTimeZoneData &rhs, |
407 | const KTimeZone &tz, const QDate &earliest) |
408 | : KTimeZoneData(rhs), |
409 | d(new ICalTimeZoneDataPrivate()) |
410 | { |
411 | // VTIMEZONE RRULE types |
412 | enum { |
413 | DAY_OF_MONTH = 0x01, |
414 | WEEKDAY_OF_MONTH = 0x02, |
415 | LAST_WEEKDAY_OF_MONTH = 0x04 |
416 | }; |
417 | |
418 | if (tz.type() == "KSystemTimeZone" ) { |
419 | // Try to fetch a system time zone in preference, on the grounds |
420 | // that system time zones are more likely to be up to date than |
421 | // built-in libical ones. |
422 | icalcomponent *c = 0; |
423 | const KTimeZone ktz = KSystemTimeZones::readZone(tz.name()); |
424 | if (ktz.isValid()) { |
425 | if (ktz.data(true)) { |
426 | const ICalTimeZone icaltz(ktz, earliest); |
427 | icaltimezone *itz = icaltz.icalTimezone(); |
428 | if (itz) { |
429 | c = icalcomponent_new_clone(icaltimezone_get_component(itz)); |
430 | icaltimezone_free(itz, 1); |
431 | } |
432 | } |
433 | } |
434 | if (!c) { |
435 | // Try to fetch a built-in libical time zone. |
436 | icaltimezone *itz = icaltimezone_get_builtin_timezone(tz.name().toUtf8()); |
437 | c = icalcomponent_new_clone(icaltimezone_get_component(itz)); |
438 | } |
439 | if (c) { |
440 | // TZID in built-in libical time zones has a standard prefix. |
441 | // To make the VTIMEZONE TZID match TZID references in incidences |
442 | // (as required by RFC2445), strip off the prefix. |
443 | icalproperty *prop = icalcomponent_get_first_property(c, ICAL_TZID_PROPERTY); |
444 | if (prop) { |
445 | icalvalue *value = icalproperty_get_value(prop); |
446 | const char *tzid = icalvalue_get_text(value); |
447 | const QByteArray icalprefix = ICalTimeZoneSource::icalTzidPrefix(); |
448 | const int len = icalprefix.size(); |
449 | if (!strncmp(icalprefix, tzid, len)) { |
450 | const char *s = strchr(tzid + len, '/'); // find third '/' |
451 | if (s) { |
452 | const QByteArray tzidShort(s + 1); // deep copy (needed by icalvalue_set_text()) |
453 | icalvalue_set_text(value, tzidShort); |
454 | |
455 | // Remove the X-LIC-LOCATION property, which is only used by libical |
456 | prop = icalcomponent_get_first_property(c, ICAL_X_PROPERTY); |
457 | const char *xname = icalproperty_get_x_name(prop); |
458 | if (xname && !strcmp(xname, "X-LIC-LOCATION" )) { |
459 | icalcomponent_remove_property(c, prop); |
460 | icalproperty_free(prop); |
461 | } |
462 | } |
463 | } |
464 | } |
465 | } |
466 | d->setComponent(c); |
467 | } else { |
468 | // Write the time zone data into an iCal component |
469 | icalcomponent *tzcomp = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT); |
470 | icalcomponent_add_property(tzcomp, icalproperty_new_tzid(tz.name().toUtf8())); |
471 | // icalcomponent_add_property(tzcomp, icalproperty_new_location( tz.name().toUtf8() )); |
472 | |
473 | // Compile an ordered list of transitions so that we can know the phases |
474 | // which occur before and after each transition. |
475 | QList<KTimeZone::Transition> transits = transitions(); |
476 | if (transits.isEmpty()) { |
477 | // If there is no way to compile a complete list of transitions |
478 | // transitions() can return an empty list |
479 | // In that case try get one transition to write a valid VTIMEZONE entry. |
480 | if (transits.isEmpty()) { |
481 | kDebug() << "No transition information available VTIMEZONE will be invalid." ; |
482 | } |
483 | } |
484 | if (earliest.isValid()) { |
485 | // Remove all transitions earlier than those we are interested in |
486 | for (int i = 0, end = transits.count(); i < end; ++i) { |
487 | if (transits.at(i).time().date() >= earliest) { |
488 | if (i > 0) { |
489 | transits.erase(transits.begin(), transits.begin() + i); |
490 | } |
491 | break; |
492 | } |
493 | } |
494 | } |
495 | int trcount = transits.count(); |
496 | QVector<bool> transitionsDone(trcount); |
497 | transitionsDone.fill(false); |
498 | |
499 | // Go through the list of transitions and create an iCal component for each |
500 | // distinct combination of phase after and UTC offset before the transition. |
501 | icaldatetimeperiodtype dtperiod; |
502 | dtperiod.period = icalperiodtype_null_period(); |
503 | for (; ;) { |
504 | int i = 0; |
505 | for (; i < trcount && transitionsDone[i]; ++i) { |
506 | ; |
507 | } |
508 | if (i >= trcount) { |
509 | break; |
510 | } |
511 | // Found a phase combination which hasn't yet been processed |
512 | const int preOffset = (i > 0) ? |
513 | transits.at(i - 1).phase().utcOffset() : |
514 | rhs.previousUtcOffset(); |
515 | const KTimeZone::Phase phase = transits.at(i).phase(); |
516 | if (phase.utcOffset() == preOffset) { |
517 | transitionsDone[i] = true; |
518 | while (++i < trcount) { |
519 | if (transitionsDone[i] || |
520 | transits.at(i).phase() != phase || |
521 | transits.at(i - 1).phase().utcOffset() != preOffset) { |
522 | continue; |
523 | } |
524 | transitionsDone[i] = true; |
525 | } |
526 | continue; |
527 | } |
528 | icalcomponent *phaseComp = |
529 | icalcomponent_new(phase.isDst() ? ICAL_XDAYLIGHT_COMPONENT : ICAL_XSTANDARD_COMPONENT); |
530 | const QList<QByteArray> abbrevs = phase.abbreviations(); |
531 | for (int a = 0, aend = abbrevs.count(); a < aend; ++a) { |
532 | icalcomponent_add_property(phaseComp, |
533 | icalproperty_new_tzname( |
534 | static_cast<const char*>(abbrevs[a]))); |
535 | } |
536 | if (!phase.comment().isEmpty()) { |
537 | icalcomponent_add_property(phaseComp, |
538 | icalproperty_new_comment(phase.comment().toUtf8())); |
539 | } |
540 | icalcomponent_add_property(phaseComp, |
541 | icalproperty_new_tzoffsetfrom(preOffset)); |
542 | icalcomponent_add_property(phaseComp, |
543 | icalproperty_new_tzoffsetto(phase.utcOffset())); |
544 | // Create a component to hold initial RRULE if any, plus all RDATEs |
545 | icalcomponent *phaseComp1 = icalcomponent_new_clone(phaseComp); |
546 | icalcomponent_add_property(phaseComp1, |
547 | icalproperty_new_dtstart( |
548 | writeLocalICalDateTime(transits.at(i).time(), |
549 | preOffset))); |
550 | bool useNewRRULE = false; |
551 | |
552 | // Compile the list of UTC transition dates/times, and check |
553 | // if the list can be reduced to an RRULE instead of multiple RDATEs. |
554 | QTime time; |
555 | QDate date; |
556 | int year = 0, month = 0, daysInMonth = 0, dayOfMonth = 0; // avoid compiler warnings |
557 | int dayOfWeek = 0; // Monday = 1 |
558 | int nthFromStart = 0; // nth (weekday) of month |
559 | int nthFromEnd = 0; // nth last (weekday) of month |
560 | int newRule; |
561 | int rule = 0; |
562 | QList<QDateTime> rdates;// dates which (probably) need to be written as RDATEs |
563 | QList<QDateTime> times; |
564 | QDateTime qdt = transits.at(i).time(); // set 'qdt' for start of loop |
565 | times += qdt; |
566 | transitionsDone[i] = true; |
567 | do { |
568 | if (!rule) { |
569 | // Initialise data for detecting a new rule |
570 | rule = DAY_OF_MONTH | WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH; |
571 | time = qdt.time(); |
572 | date = qdt.date(); |
573 | year = date.year(); |
574 | month = date.month(); |
575 | daysInMonth = date.daysInMonth(); |
576 | dayOfWeek = date.dayOfWeek(); // Monday = 1 |
577 | dayOfMonth = date.day(); |
578 | nthFromStart = (dayOfMonth - 1) / 7 + 1; // nth (weekday) of month |
579 | nthFromEnd = (daysInMonth - dayOfMonth) / 7 + 1; // nth last (weekday) of month |
580 | } |
581 | if (++i >= trcount) { |
582 | newRule = 0; |
583 | times += QDateTime(); // append a dummy value since last value in list is ignored |
584 | } else { |
585 | if (transitionsDone[i] || |
586 | transits.at(i).phase() != phase || |
587 | transits.at(i - 1).phase().utcOffset() != preOffset) { |
588 | continue; |
589 | } |
590 | transitionsDone[i] = true; |
591 | qdt = transits.at(i).time(); |
592 | if (!qdt.isValid()) { |
593 | continue; |
594 | } |
595 | newRule = rule; |
596 | times += qdt; |
597 | date = qdt.date(); |
598 | if (qdt.time() != time || |
599 | date.month() != month || |
600 | date.year() != ++year) { |
601 | newRule = 0; |
602 | } else { |
603 | const int day = date.day(); |
604 | if ((newRule & DAY_OF_MONTH) && day != dayOfMonth) { |
605 | newRule &= ~DAY_OF_MONTH; |
606 | } |
607 | if (newRule & (WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH)) { |
608 | if (date.dayOfWeek() != dayOfWeek) { |
609 | newRule &= ~(WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH); |
610 | } else { |
611 | if ((newRule & WEEKDAY_OF_MONTH) && |
612 | (day - 1) / 7 + 1 != nthFromStart) { |
613 | newRule &= ~WEEKDAY_OF_MONTH; |
614 | } |
615 | if ((newRule & LAST_WEEKDAY_OF_MONTH) && |
616 | (daysInMonth - day) / 7 + 1 != nthFromEnd) { |
617 | newRule &= ~LAST_WEEKDAY_OF_MONTH; |
618 | } |
619 | } |
620 | } |
621 | } |
622 | } |
623 | if (!newRule) { |
624 | // The previous rule (if any) no longer applies. |
625 | // Write all the times up to but not including the current one. |
626 | // First check whether any of the last RDATE values fit this rule. |
627 | int yr = times[0].date().year(); |
628 | while (!rdates.isEmpty()) { |
629 | qdt = rdates.last(); |
630 | date = qdt.date(); |
631 | if (qdt.time() != time || |
632 | date.month() != month || |
633 | date.year() != --yr) { |
634 | break; |
635 | } |
636 | const int day = date.day(); |
637 | if (rule & DAY_OF_MONTH) { |
638 | if (day != dayOfMonth) { |
639 | break; |
640 | } |
641 | } else { |
642 | if (date.dayOfWeek() != dayOfWeek || |
643 | ((rule & WEEKDAY_OF_MONTH) && |
644 | (day - 1) / 7 + 1 != nthFromStart) || |
645 | ((rule & LAST_WEEKDAY_OF_MONTH) && |
646 | (daysInMonth - day) / 7 + 1 != nthFromEnd)) { |
647 | break; |
648 | } |
649 | } |
650 | times.prepend(qdt); |
651 | rdates.pop_back(); |
652 | } |
653 | if (times.count() > (useNewRRULE ? minPhaseCount : minRuleCount)) { |
654 | // There are enough dates to combine into an RRULE |
655 | icalrecurrencetype r; |
656 | icalrecurrencetype_clear(&r); |
657 | r.freq = ICAL_YEARLY_RECURRENCE; |
658 | r.count = (year >= 2030) ? 0 : times.count() - 1; |
659 | r.by_month[0] = month; |
660 | if (rule & DAY_OF_MONTH) { |
661 | r.by_month_day[0] = dayOfMonth; |
662 | } else if (rule & WEEKDAY_OF_MONTH) { |
663 | r.by_day[0] = (dayOfWeek % 7 + 1) + (nthFromStart * 8); // Sunday = 1 |
664 | } else if (rule & LAST_WEEKDAY_OF_MONTH) { |
665 | r.by_day[0] = -(dayOfWeek % 7 + 1) - (nthFromEnd * 8); // Sunday = 1 |
666 | } |
667 | icalproperty *prop = icalproperty_new_rrule(r); |
668 | if (useNewRRULE) { |
669 | // This RRULE doesn't start from the phase start date, so set it into |
670 | // a new STANDARD/DAYLIGHT component in the VTIMEZONE. |
671 | icalcomponent *c = icalcomponent_new_clone(phaseComp); |
672 | icalcomponent_add_property( |
673 | c, icalproperty_new_dtstart(writeLocalICalDateTime(times[0], preOffset))); |
674 | icalcomponent_add_property(c, prop); |
675 | icalcomponent_add_component(tzcomp, c); |
676 | } else { |
677 | icalcomponent_add_property(phaseComp1, prop); |
678 | } |
679 | } else { |
680 | // Save dates for writing as RDATEs |
681 | for (int t = 0, tend = times.count() - 1; t < tend; ++t) { |
682 | rdates += times[t]; |
683 | } |
684 | } |
685 | useNewRRULE = true; |
686 | // All date/time values but the last have been added to the VTIMEZONE. |
687 | // Remove them from the list. |
688 | qdt = times.last(); // set 'qdt' for start of loop |
689 | times.clear(); |
690 | times += qdt; |
691 | } |
692 | rule = newRule; |
693 | } while (i < trcount); |
694 | |
695 | // Write remaining dates as RDATEs |
696 | for (int rd = 0, rdend = rdates.count(); rd < rdend; ++rd) { |
697 | dtperiod.time = writeLocalICalDateTime(rdates[rd], preOffset); |
698 | icalcomponent_add_property(phaseComp1, icalproperty_new_rdate(dtperiod)); |
699 | } |
700 | icalcomponent_add_component(tzcomp, phaseComp1); |
701 | icalcomponent_free(phaseComp); |
702 | } |
703 | |
704 | d->setComponent(tzcomp); |
705 | } |
706 | } |
707 | |
708 | ICalTimeZoneData::~ICalTimeZoneData() |
709 | { |
710 | delete d; |
711 | } |
712 | |
713 | ICalTimeZoneData &ICalTimeZoneData::operator=(const ICalTimeZoneData &rhs) |
714 | { |
715 | // check for self assignment |
716 | if (&rhs == this) { |
717 | return *this; |
718 | } |
719 | |
720 | KTimeZoneData::operator=(rhs); |
721 | d->location = rhs.d->location; |
722 | d->url = rhs.d->url; |
723 | d->lastModified = rhs.d->lastModified; |
724 | d->setComponent(icalcomponent_new_clone(rhs.d->component())); |
725 | return *this; |
726 | } |
727 | |
728 | KTimeZoneData *ICalTimeZoneData::clone() const |
729 | { |
730 | return new ICalTimeZoneData(*this); |
731 | } |
732 | |
733 | QString ICalTimeZoneData::city() const |
734 | { |
735 | return d->location; |
736 | } |
737 | |
738 | QByteArray ICalTimeZoneData::url() const |
739 | { |
740 | return d->url; |
741 | } |
742 | |
743 | QDateTime ICalTimeZoneData::lastModified() const |
744 | { |
745 | return d->lastModified; |
746 | } |
747 | |
748 | QByteArray ICalTimeZoneData::vtimezone() const |
749 | { |
750 | const QByteArray result(icalcomponent_as_ical_string(d->component())); |
751 | icalmemory_free_ring(); |
752 | return result; |
753 | } |
754 | |
755 | icaltimezone *ICalTimeZoneData::icalTimezone() const |
756 | { |
757 | icaltimezone *icaltz = icaltimezone_new(); |
758 | if (!icaltz) { |
759 | return 0; |
760 | } |
761 | icalcomponent *c = icalcomponent_new_clone(d->component()); |
762 | if (!icaltimezone_set_component(icaltz, c)) { |
763 | icalcomponent_free(c); |
764 | icaltimezone_free(icaltz, 1); |
765 | return 0; |
766 | } |
767 | return icaltz; |
768 | } |
769 | |
770 | bool ICalTimeZoneData::hasTransitions() const |
771 | { |
772 | return true; |
773 | } |
774 | |
775 | void ICalTimeZoneData::virtual_hook(int id, void *data) |
776 | { |
777 | Q_UNUSED(id); |
778 | Q_UNUSED(data); |
779 | } |
780 | |
781 | /******************************************************************************/ |
782 | |
783 | //@cond PRIVATE |
784 | class ICalTimeZoneSourcePrivate |
785 | { |
786 | public: |
787 | static QList<QDateTime> parsePhase(icalcomponent *, bool daylight, |
788 | int &prevOffset, KTimeZone::Phase &); |
789 | static QByteArray icalTzidPrefix; |
790 | |
791 | #if defined(HAVE_UUID_UUID_H) |
792 | static void parseTransitions(const MSSystemTime &date, const KTimeZone::Phase &phase, |
793 | int prevOffset, QList<KTimeZone::Transition> &transitions); |
794 | #endif |
795 | }; |
796 | |
797 | QByteArray ICalTimeZoneSourcePrivate::icalTzidPrefix; |
798 | //@endcond |
799 | |
800 | ICalTimeZoneSource::ICalTimeZoneSource() |
801 | : KTimeZoneSource(false), |
802 | d(0) |
803 | { |
804 | Q_UNUSED(d); |
805 | } |
806 | |
807 | ICalTimeZoneSource::~ICalTimeZoneSource() |
808 | { |
809 | } |
810 | |
811 | bool ICalTimeZoneSource::parse(const QString &fileName, ICalTimeZones &zones) |
812 | { |
813 | QFile file(fileName); |
814 | if (!file.open(QIODevice::ReadOnly)) { |
815 | return false; |
816 | } |
817 | QTextStream ts(&file); |
818 | ts.setCodec("ISO 8859-1" ); |
819 | const QByteArray text = ts.readAll().trimmed().toLatin1(); |
820 | file.close(); |
821 | |
822 | bool result = false; |
823 | icalcomponent *calendar = icalcomponent_new_from_string(text.data()); |
824 | if (calendar) { |
825 | if (icalcomponent_isa(calendar) == ICAL_VCALENDAR_COMPONENT) { |
826 | result = parse(calendar, zones); |
827 | } |
828 | icalcomponent_free(calendar); |
829 | } |
830 | return result; |
831 | } |
832 | |
833 | bool ICalTimeZoneSource::parse(icalcomponent *calendar, ICalTimeZones &zones) |
834 | { |
835 | for (icalcomponent *c = icalcomponent_get_first_component(calendar, ICAL_VTIMEZONE_COMPONENT); |
836 | c; c = icalcomponent_get_next_component(calendar, ICAL_VTIMEZONE_COMPONENT)) { |
837 | const ICalTimeZone zone = parse(c); |
838 | if (!zone.isValid()) { |
839 | return false; |
840 | } |
841 | ICalTimeZone oldzone = zones.zone(zone.name()); |
842 | if (oldzone.isValid()) { |
843 | // The zone already exists in the collection, so update the definition |
844 | // of the zone rather than using a newly created one. |
845 | oldzone.update(zone); |
846 | } else if (!zones.add(zone)) { |
847 | return false; |
848 | } |
849 | } |
850 | return true; |
851 | } |
852 | |
853 | ICalTimeZone ICalTimeZoneSource::parse(icalcomponent *vtimezone) |
854 | { |
855 | QString name; |
856 | QString xlocation; |
857 | ICalTimeZoneData *data = new ICalTimeZoneData(); |
858 | |
859 | // Read the fixed properties which can only appear once in VTIMEZONE |
860 | icalproperty *p = icalcomponent_get_first_property(vtimezone, ICAL_ANY_PROPERTY); |
861 | while (p) { |
862 | icalproperty_kind kind = icalproperty_isa(p); |
863 | switch (kind) { |
864 | |
865 | case ICAL_TZID_PROPERTY: |
866 | name = QString::fromUtf8(icalproperty_get_tzid(p)); |
867 | break; |
868 | |
869 | case ICAL_TZURL_PROPERTY: |
870 | data->d->url = icalproperty_get_tzurl(p); |
871 | break; |
872 | |
873 | case ICAL_LOCATION_PROPERTY: |
874 | // This isn't mentioned in RFC2445, but libical reads it ... |
875 | data->d->location = QString::fromUtf8(icalproperty_get_location(p)); |
876 | break; |
877 | |
878 | case ICAL_X_PROPERTY: |
879 | { // use X-LIC-LOCATION if LOCATION is missing |
880 | const char *xname = icalproperty_get_x_name(p); |
881 | if (xname && !strcmp(xname, "X-LIC-LOCATION" )) { |
882 | xlocation = QString::fromUtf8(icalproperty_get_x(p)); |
883 | } |
884 | break; |
885 | } |
886 | case ICAL_LASTMODIFIED_PROPERTY: |
887 | { |
888 | const icaltimetype t = icalproperty_get_lastmodified(p); |
889 | if (t.is_utc) { |
890 | data->d->lastModified = toQDateTime(t); |
891 | } else { |
892 | kDebug() << "LAST-MODIFIED not UTC" ; |
893 | } |
894 | break; |
895 | } |
896 | default: |
897 | break; |
898 | } |
899 | p = icalcomponent_get_next_property(vtimezone, ICAL_ANY_PROPERTY); |
900 | } |
901 | |
902 | if (name.isEmpty()) { |
903 | kDebug() << "TZID missing" ; |
904 | delete data; |
905 | return ICalTimeZone(); |
906 | } |
907 | if (data->d->location.isEmpty() && !xlocation.isEmpty()) { |
908 | data->d->location = xlocation; |
909 | } |
910 | const QString prefix = QString::fromUtf8(icalTzidPrefix()); |
911 | if (name.startsWith(prefix)) { |
912 | // Remove the prefix from libical built in time zone TZID |
913 | const int i = name.indexOf(QLatin1Char('/'), prefix.length()); |
914 | if (i > 0) { |
915 | name = name.mid(i + 1); |
916 | } |
917 | } |
918 | //kDebug() << "---zoneId: \"" << name << '"'; |
919 | |
920 | /* |
921 | * Iterate through all time zone rules for this VTIMEZONE, |
922 | * and create a Phase object containing details for each one. |
923 | */ |
924 | int prevOffset = 0; |
925 | QList<KTimeZone::Transition> transitions; |
926 | QDateTime earliest; |
927 | QList<KTimeZone::Phase> phases; |
928 | for (icalcomponent *c = icalcomponent_get_first_component(vtimezone, ICAL_ANY_COMPONENT); |
929 | c; c = icalcomponent_get_next_component(vtimezone, ICAL_ANY_COMPONENT)) { |
930 | int prevoff = 0; |
931 | KTimeZone::Phase phase; |
932 | QList<QDateTime> times; |
933 | icalcomponent_kind kind = icalcomponent_isa(c); |
934 | switch (kind) { |
935 | |
936 | case ICAL_XSTANDARD_COMPONENT: |
937 | //kDebug() << "---standard phase: found"; |
938 | times = ICalTimeZoneSourcePrivate::parsePhase(c, false, prevoff, phase); |
939 | break; |
940 | |
941 | case ICAL_XDAYLIGHT_COMPONENT: |
942 | //kDebug() << "---daylight phase: found"; |
943 | times = ICalTimeZoneSourcePrivate::parsePhase(c, true, prevoff, phase); |
944 | break; |
945 | |
946 | default: |
947 | kDebug() << "Unknown component:" << int(kind); |
948 | break; |
949 | } |
950 | const int tcount = times.count(); |
951 | if (tcount) { |
952 | phases += phase; |
953 | for (int t = 0; t < tcount; ++t) { |
954 | transitions += KTimeZone::Transition(times[t], phase); |
955 | } |
956 | if (!earliest.isValid() || times[0] < earliest) { |
957 | prevOffset = prevoff; |
958 | earliest = times[0]; |
959 | } |
960 | } |
961 | } |
962 | // Set phases used by the time zone, but note that VTIMEZONE doesn't contain |
963 | // time zone abbreviation before first transition. |
964 | data->setPhases(phases, prevOffset); |
965 | // Remove any "duplicate" transitions, i.e. those where two consecutive |
966 | // transitions have the same phase. |
967 | qSort(transitions); |
968 | for (int t = 1, tend = transitions.count(); t < tend;) { |
969 | if (transitions[t].phase() == transitions[t - 1].phase()) { |
970 | transitions.removeAt(t); |
971 | --tend; |
972 | } else { |
973 | ++t; |
974 | } |
975 | } |
976 | data->setTransitions(transitions); |
977 | |
978 | data->d->setComponent(icalcomponent_new_clone(vtimezone)); |
979 | //kDebug() << "VTIMEZONE" << name; |
980 | return ICalTimeZone(this, name, data); |
981 | } |
982 | |
983 | #if defined(HAVE_UUID_UUID_H) |
984 | ICalTimeZone ICalTimeZoneSource::parse(MSTimeZone *tz, ICalTimeZones &zones) |
985 | { |
986 | const ICalTimeZone zone = parse(tz); |
987 | if (!zone.isValid()) { |
988 | return ICalTimeZone(); // error |
989 | } |
990 | const ICalTimeZone oldzone = zones.zone(zone); |
991 | if (oldzone.isValid()) { |
992 | // A similar zone already exists in the collection, so don't add this |
993 | // new zone, return old zone instead. |
994 | return oldzone; |
995 | } else if (zones.add(zone)) { |
996 | // No similar zone, add and return new one. |
997 | return zone; |
998 | } |
999 | return ICalTimeZone(); // error |
1000 | } |
1001 | |
1002 | ICalTimeZone ICalTimeZoneSource::parse(MSTimeZone *tz) |
1003 | { |
1004 | ICalTimeZoneData kdata; |
1005 | |
1006 | // General properties. |
1007 | uuid_t uuid; |
1008 | char suuid[64]; |
1009 | uuid_generate_random(uuid); |
1010 | uuid_unparse(uuid, suuid); |
1011 | QString name = QString::fromLatin1(suuid); |
1012 | |
1013 | // Create phases. |
1014 | QList<KTimeZone::Phase> phases; |
1015 | |
1016 | QList<QByteArray> standardAbbrevs; |
1017 | standardAbbrevs += tz->StandardName.toLatin1(); |
1018 | const KTimeZone::Phase standardPhase( |
1019 | (tz->Bias + tz->StandardBias) * -60, |
1020 | standardAbbrevs, false, |
1021 | QLatin1String("Microsoft TIME_ZONE_INFORMATION" )); |
1022 | phases += standardPhase; |
1023 | |
1024 | QList<QByteArray> daylightAbbrevs; |
1025 | daylightAbbrevs += tz->DaylightName.toLatin1(); |
1026 | const KTimeZone::Phase daylightPhase( |
1027 | (tz->Bias + tz->DaylightBias) * -60, |
1028 | daylightAbbrevs, true, |
1029 | QLatin1String("Microsoft TIME_ZONE_INFORMATION" )); |
1030 | phases += daylightPhase; |
1031 | |
1032 | // Set phases used by the time zone, but note that previous time zone |
1033 | // abbreviation is not known. |
1034 | const int prevOffset = tz->Bias * -60; |
1035 | kdata.setPhases(phases, prevOffset); |
1036 | |
1037 | // Create transitions |
1038 | QList<KTimeZone::Transition> transitions; |
1039 | ICalTimeZoneSourcePrivate::parseTransitions( |
1040 | tz->StandardDate, standardPhase, prevOffset, transitions); |
1041 | ICalTimeZoneSourcePrivate::parseTransitions( |
1042 | tz->DaylightDate, daylightPhase, prevOffset, transitions); |
1043 | |
1044 | qSort(transitions); |
1045 | kdata.setTransitions(transitions); |
1046 | |
1047 | ICalTimeZoneData *idata = new ICalTimeZoneData(kdata, KTimeZone(name), QDate()); |
1048 | |
1049 | return ICalTimeZone(this, name, idata); |
1050 | } |
1051 | #endif // HAVE_UUID_UUID_H |
1052 | |
1053 | ICalTimeZone ICalTimeZoneSource::parse(const QString &name, const QStringList &tzList, |
1054 | ICalTimeZones &zones) |
1055 | { |
1056 | const ICalTimeZone zone = parse(name, tzList); |
1057 | if (!zone.isValid()) { |
1058 | return ICalTimeZone(); // error |
1059 | } |
1060 | |
1061 | ICalTimeZone oldzone = zones.zone(zone); |
1062 | // First off see if the zone is same as oldzone - _exactly_ same |
1063 | if (oldzone.isValid()) { |
1064 | return oldzone; |
1065 | } |
1066 | |
1067 | oldzone = zones.zone(name); |
1068 | if (oldzone.isValid()) { |
1069 | // The zone already exists, so update |
1070 | oldzone.update(zone); |
1071 | return zone; |
1072 | } else if (zones.add(zone)) { |
1073 | // No similar zone, add and return new one. |
1074 | return zone; |
1075 | } |
1076 | return ICalTimeZone(); // error |
1077 | } |
1078 | |
1079 | ICalTimeZone ICalTimeZoneSource::parse(const QString &name, const QStringList &tzList) |
1080 | { |
1081 | ICalTimeZoneData kdata; |
1082 | QList<KTimeZone::Phase> phases; |
1083 | QList<KTimeZone::Transition> transitions; |
1084 | bool daylight; |
1085 | |
1086 | for (QStringList::ConstIterator it = tzList.begin(); it != tzList.end(); ++it) { |
1087 | QString value = *it; |
1088 | daylight = false; |
1089 | const QString tzName = value.mid(0, value.indexOf(QLatin1String(";" ))); |
1090 | value = value.mid((value.indexOf(QLatin1String(";" )) + 1)); |
1091 | const QString tzOffset = value.mid(0, value.indexOf(QLatin1String(";" ))); |
1092 | value = value.mid((value.indexOf(QLatin1String(";" )) + 1)); |
1093 | const QString tzDaylight = value.mid(0, value.indexOf(QLatin1String(";" ))); |
1094 | const KDateTime tzDate = KDateTime::fromString(value.mid((value.lastIndexOf(QLatin1String(";" )) + 1))); |
1095 | if (tzDaylight == QLatin1String("true" )) { |
1096 | daylight = true; |
1097 | } |
1098 | |
1099 | const KTimeZone::Phase tzPhase( |
1100 | tzOffset.toInt(), |
1101 | QByteArray(tzName.toLatin1()), daylight, QLatin1String("VCAL_TZ_INFORMATION" )); |
1102 | phases += tzPhase; |
1103 | transitions += KTimeZone::Transition(tzDate.dateTime(), tzPhase); |
1104 | } |
1105 | |
1106 | kdata.setPhases(phases, 0); |
1107 | qSort(transitions); |
1108 | kdata.setTransitions(transitions); |
1109 | |
1110 | ICalTimeZoneData *idata = new ICalTimeZoneData(kdata, KTimeZone(name), QDate()); |
1111 | return ICalTimeZone(this, name, idata); |
1112 | } |
1113 | |
1114 | #if defined(HAVE_UUID_UUID_H) |
1115 | //@cond PRIVATE |
1116 | void ICalTimeZoneSourcePrivate::parseTransitions(const MSSystemTime &date, |
1117 | const KTimeZone::Phase &phase, int prevOffset, |
1118 | QList<KTimeZone::Transition> &transitions) |
1119 | { |
1120 | // NOTE that we need to set start and end times and they cannot be |
1121 | // to far in either direction to avoid bloating the transitions list |
1122 | const KDateTime klocalStart(QDateTime(QDate(2000, 1, 1), QTime(0, 0, 0)), |
1123 | KDateTime::Spec::ClockTime()); |
1124 | const KDateTime maxTime(MAX_DATE(), KDateTime::Spec::ClockTime()); |
1125 | |
1126 | if (date.wYear) { |
1127 | // Absolute change time. |
1128 | if (date.wYear >= 1601 && date.wYear <= 30827 && |
1129 | date.wMonth >= 1 && date.wMonth <= 12 && |
1130 | date.wDay >= 1 && date.wDay <= 31) { |
1131 | const QDate dt(date.wYear, date.wMonth, date.wDay); |
1132 | const QTime tm(date.wHour, date.wMinute, date.wSecond, date.wMilliseconds); |
1133 | const QDateTime datetime(dt, tm); |
1134 | if (datetime.isValid()) { |
1135 | transitions += KTimeZone::Transition(datetime, phase); |
1136 | } |
1137 | } |
1138 | } else { |
1139 | // The normal way, for example: 'First Sunday in April at 02:00'. |
1140 | if (date.wDayOfWeek >= 0 && date.wDayOfWeek <= 6 && |
1141 | date.wMonth >= 1 && date.wMonth <= 12 && |
1142 | date.wDay >= 1 && date.wDay <= 5) { |
1143 | RecurrenceRule r; |
1144 | r.setRecurrenceType(RecurrenceRule::rYearly); |
1145 | r.setDuration(-1); |
1146 | r.setFrequency(1); |
1147 | QList<int> lst; |
1148 | lst.append(date.wMonth); |
1149 | r.setByMonths(lst); |
1150 | QList<RecurrenceRule::WDayPos> wdlst; |
1151 | RecurrenceRule::WDayPos pos; |
1152 | pos.setDay(date.wDayOfWeek ? date.wDayOfWeek : 7); |
1153 | pos.setPos(date.wDay < 5 ? date.wDay : -1); |
1154 | wdlst.append(pos); |
1155 | r.setByDays(wdlst); |
1156 | r.setStartDt(klocalStart); |
1157 | r.setWeekStart(1); |
1158 | const DateTimeList dtl = r.timesInInterval(klocalStart, maxTime); |
1159 | for (int i = 0, end = dtl.count(); i < end; ++i) { |
1160 | QDateTime utc = dtl[i].dateTime(); |
1161 | utc.setTimeSpec(Qt::UTC); |
1162 | transitions += KTimeZone::Transition(utc.addSecs(-prevOffset), phase); |
1163 | } |
1164 | } |
1165 | } |
1166 | } |
1167 | //@endcond |
1168 | #endif // HAVE_UUID_UUID_H |
1169 | |
1170 | ICalTimeZone ICalTimeZoneSource::parse(icaltimezone *tz) |
1171 | { |
1172 | /* Parse the VTIMEZONE component stored in the icaltimezone structure. |
1173 | * This is both easier and provides more complete information than |
1174 | * extracting already parsed data from icaltimezone. |
1175 | */ |
1176 | return tz ? parse(icaltimezone_get_component(tz)) : ICalTimeZone(); |
1177 | } |
1178 | |
1179 | //@cond PRIVATE |
1180 | QList<QDateTime> ICalTimeZoneSourcePrivate::parsePhase(icalcomponent *c, |
1181 | bool daylight, |
1182 | int &prevOffset, |
1183 | KTimeZone::Phase &phase) |
1184 | { |
1185 | QList<QDateTime> transitions; |
1186 | |
1187 | // Read the observance data for this standard/daylight savings phase |
1188 | QList<QByteArray> abbrevs; |
1189 | QString ; |
1190 | prevOffset = 0; |
1191 | int utcOffset = 0; |
1192 | bool recurs = false; |
1193 | bool found_dtstart = false; |
1194 | bool found_tzoffsetfrom = false; |
1195 | bool found_tzoffsetto = false; |
1196 | icaltimetype dtstart = icaltime_null_time(); |
1197 | |
1198 | // Now do the ical reading. |
1199 | icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY); |
1200 | while (p) { |
1201 | icalproperty_kind kind = icalproperty_isa(p); |
1202 | switch (kind) { |
1203 | |
1204 | case ICAL_TZNAME_PROPERTY: // abbreviated name for this time offset |
1205 | { |
1206 | // TZNAME can appear multiple times in order to provide language |
1207 | // translations of the time zone offset name. |
1208 | |
1209 | // TODO: Does this cope with multiple language specifications? |
1210 | QByteArray tzname = icalproperty_get_tzname(p); |
1211 | // Outlook (2000) places "Standard Time" and "Daylight Time" in the TZNAME |
1212 | // strings, which is totally useless. So ignore those. |
1213 | if ((!daylight && tzname == "Standard Time" ) || |
1214 | (daylight && tzname == "Daylight Time" )) { |
1215 | break; |
1216 | } |
1217 | if (!abbrevs.contains(tzname)) { |
1218 | abbrevs += tzname; |
1219 | } |
1220 | break; |
1221 | } |
1222 | case ICAL_DTSTART_PROPERTY: // local time at which phase starts |
1223 | dtstart = icalproperty_get_dtstart(p); |
1224 | found_dtstart = true; |
1225 | break; |
1226 | |
1227 | case ICAL_TZOFFSETFROM_PROPERTY: // UTC offset immediately before start of phase |
1228 | prevOffset = icalproperty_get_tzoffsetfrom(p); |
1229 | found_tzoffsetfrom = true; |
1230 | break; |
1231 | |
1232 | case ICAL_TZOFFSETTO_PROPERTY: |
1233 | utcOffset = icalproperty_get_tzoffsetto(p); |
1234 | found_tzoffsetto = true; |
1235 | break; |
1236 | |
1237 | case ICAL_COMMENT_PROPERTY: |
1238 | comment = QString::fromUtf8(icalproperty_get_comment(p)); |
1239 | break; |
1240 | |
1241 | case ICAL_RDATE_PROPERTY: |
1242 | case ICAL_RRULE_PROPERTY: |
1243 | recurs = true; |
1244 | break; |
1245 | |
1246 | default: |
1247 | kDebug() << "Unknown property:" << int(kind); |
1248 | break; |
1249 | } |
1250 | p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY); |
1251 | } |
1252 | |
1253 | // Validate the phase data |
1254 | if (!found_dtstart || !found_tzoffsetfrom || !found_tzoffsetto) { |
1255 | kDebug() << "DTSTART/TZOFFSETFROM/TZOFFSETTO missing" ; |
1256 | return transitions; |
1257 | } |
1258 | |
1259 | // Convert DTSTART to QDateTime, and from local time to UTC |
1260 | const QDateTime localStart = toQDateTime(dtstart); // local time |
1261 | dtstart.second -= prevOffset; |
1262 | dtstart.is_utc = 1; |
1263 | const QDateTime utcStart = toQDateTime(icaltime_normalize(dtstart)); // UTC |
1264 | |
1265 | transitions += utcStart; |
1266 | if (recurs) { |
1267 | /* RDATE or RRULE is specified. There should only be one or the other, but |
1268 | * it doesn't really matter - the code can cope with both. |
1269 | * Note that we had to get DTSTART, TZOFFSETFROM, TZOFFSETTO before reading |
1270 | * recurrences. |
1271 | */ |
1272 | const KDateTime klocalStart(localStart, KDateTime::Spec::ClockTime()); |
1273 | const KDateTime maxTime(MAX_DATE(), KDateTime::Spec::ClockTime()); |
1274 | Recurrence recur; |
1275 | icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY); |
1276 | while (p) { |
1277 | icalproperty_kind kind = icalproperty_isa(p); |
1278 | switch (kind) { |
1279 | |
1280 | case ICAL_RDATE_PROPERTY: |
1281 | { |
1282 | icaltimetype t = icalproperty_get_rdate(p).time; |
1283 | if (icaltime_is_date(t)) { |
1284 | // RDATE with a DATE value inherits the (local) time from DTSTART |
1285 | t.hour = dtstart.hour; |
1286 | t.minute = dtstart.minute; |
1287 | t.second = dtstart.second; |
1288 | t.is_date = 0; |
1289 | t.is_utc = 0; // dtstart is in local time |
1290 | } |
1291 | // RFC2445 states that RDATE must be in local time, |
1292 | // but we support UTC as well to be safe. |
1293 | if (!t.is_utc) { |
1294 | t.second -= prevOffset; // convert to UTC |
1295 | t.is_utc = 1; |
1296 | t = icaltime_normalize(t); |
1297 | } |
1298 | transitions += toQDateTime(t); |
1299 | break; |
1300 | } |
1301 | case ICAL_RRULE_PROPERTY: |
1302 | { |
1303 | RecurrenceRule r; |
1304 | ICalFormat icf; |
1305 | ICalFormatImpl impl(&icf); |
1306 | impl.readRecurrence(icalproperty_get_rrule(p), &r); |
1307 | r.setStartDt(klocalStart); |
1308 | // The end date time specified in an RRULE should be in UTC. |
1309 | // Convert to local time to avoid timesInInterval() getting things wrong. |
1310 | if (r.duration() == 0) { |
1311 | KDateTime end(r.endDt()); |
1312 | if (end.timeSpec() == KDateTime::Spec::UTC()) { |
1313 | end.setTimeSpec(KDateTime::Spec::ClockTime()); |
1314 | r.setEndDt(end.addSecs(prevOffset)); |
1315 | } |
1316 | } |
1317 | const DateTimeList dts = r.timesInInterval(klocalStart, maxTime); |
1318 | for (int i = 0, end = dts.count(); i < end; ++i) { |
1319 | QDateTime utc = dts[i].dateTime(); |
1320 | utc.setTimeSpec(Qt::UTC); |
1321 | transitions += utc.addSecs(-prevOffset); |
1322 | } |
1323 | break; |
1324 | } |
1325 | default: |
1326 | break; |
1327 | } |
1328 | p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY); |
1329 | } |
1330 | qSortUnique(transitions); |
1331 | } |
1332 | |
1333 | phase = KTimeZone::Phase(utcOffset, abbrevs, daylight, comment); |
1334 | return transitions; |
1335 | } |
1336 | //@endcond |
1337 | |
1338 | ICalTimeZone ICalTimeZoneSource::standardZone(const QString &zone, bool icalBuiltIn) |
1339 | { |
1340 | if (!icalBuiltIn) { |
1341 | // Try to fetch a system time zone in preference, on the grounds |
1342 | // that system time zones are more likely to be up to date than |
1343 | // built-in libical ones. |
1344 | QString tzid = zone; |
1345 | const QString prefix = QString::fromUtf8(icalTzidPrefix()); |
1346 | if (zone.startsWith(prefix)) { |
1347 | const int i = zone.indexOf(QLatin1Char('/'), prefix.length()); |
1348 | if (i > 0) { |
1349 | tzid = zone.mid(i + 1); // strip off the libical prefix |
1350 | } |
1351 | } |
1352 | const KTimeZone ktz = KSystemTimeZones::readZone(tzid); |
1353 | if (ktz.isValid()) { |
1354 | if (ktz.data(true)) { |
1355 | const ICalTimeZone icaltz(ktz); |
1356 | //kDebug() << zone << " read from system database"; |
1357 | return icaltz; |
1358 | } |
1359 | } |
1360 | } |
1361 | // Try to fetch a built-in libical time zone. |
1362 | // First try to look it up as a geographical location (e.g. Europe/London) |
1363 | const QByteArray zoneName = zone.toUtf8(); |
1364 | icaltimezone *icaltz = icaltimezone_get_builtin_timezone(zoneName); |
1365 | if (!icaltz) { |
1366 | // This will find it if it includes the libical prefix |
1367 | icaltz = icaltimezone_get_builtin_timezone_from_tzid(zoneName); |
1368 | if (!icaltz) { |
1369 | return ICalTimeZone(); |
1370 | } |
1371 | } |
1372 | return parse(icaltz); |
1373 | } |
1374 | |
1375 | QByteArray ICalTimeZoneSource::icalTzidPrefix() |
1376 | { |
1377 | if (ICalTimeZoneSourcePrivate::icalTzidPrefix.isEmpty()) { |
1378 | icaltimezone *icaltz = icaltimezone_get_builtin_timezone("Europe/London" ); |
1379 | const QByteArray tzid = icaltimezone_get_tzid(icaltz); |
1380 | if (tzid.right(13) == "Europe/London" ) { |
1381 | int i = tzid.indexOf('/', 1); |
1382 | if (i > 0) { |
1383 | ICalTimeZoneSourcePrivate::icalTzidPrefix = tzid.left(i + 1); |
1384 | return ICalTimeZoneSourcePrivate::icalTzidPrefix; |
1385 | } |
1386 | } |
1387 | kError() << "failed to get libical TZID prefix" ; |
1388 | } |
1389 | return ICalTimeZoneSourcePrivate::icalTzidPrefix; |
1390 | } |
1391 | |
1392 | void ICalTimeZoneSource::virtual_hook(int id, void *data) |
1393 | { |
1394 | Q_UNUSED(id); |
1395 | Q_UNUSED(data); |
1396 | Q_ASSERT(false); |
1397 | } |
1398 | |
1399 | } // namespace KCalCore |
1400 | |