mirror of
https://bitbucket.org/smil3y/kde-playground.git
synced 2025-02-23 10:22:50 +00:00
1105 lines
35 KiB
C++
1105 lines
35 KiB
C++
/*
|
|
This file is part of the kcal library.
|
|
|
|
Copyright (c) 2005-2007 David Jarvie <djarvie@kde.org>
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Library General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2 of the License, or (at your option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Library General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Library General Public License
|
|
along with this library; see the file COPYING.LIB. If not, write to
|
|
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "icaltimezones.h"
|
|
#include "icalformat.h"
|
|
#include "icalformat_p.h"
|
|
|
|
extern "C" {
|
|
#include <libical/ical.h>
|
|
#include <libical/icaltimezone.h>
|
|
}
|
|
#include <ksystemtimezone.h>
|
|
#include <kdatetime.h>
|
|
#include <kdebug.h>
|
|
|
|
#include <QtCore/QDateTime>
|
|
#include <QtCore/QString>
|
|
#include <QtCore/QList>
|
|
#include <QtCore/QVector>
|
|
#include <QtCore/QSet>
|
|
#include <QtCore/QFile>
|
|
#include <QtCore/QTextStream>
|
|
|
|
using namespace KCal;
|
|
|
|
// Minimum repetition counts for VTIMEZONE RRULEs
|
|
static const int minRuleCount = 5; // for any RRULE
|
|
static const int minPhaseCount = 8; // for separate STANDARD/DAYLIGHT component
|
|
|
|
// Convert an ical time to QDateTime, preserving the UTC indicator
|
|
static QDateTime toQDateTime( const icaltimetype &t )
|
|
{
|
|
return QDateTime( QDate( t.year, t.month, t.day ),
|
|
QTime( t.hour, t.minute, t.second ),
|
|
( t.is_utc ? Qt::UTC : Qt::LocalTime ) );
|
|
}
|
|
|
|
// Maximum date for time zone data.
|
|
// It's not sensible to try to predict them very far in advance, because
|
|
// they can easily change. Plus, it limits the processing required.
|
|
static QDateTime MAX_DATE()
|
|
{
|
|
static QDateTime dt;
|
|
if ( !dt.isValid() ) {
|
|
dt = QDateTime( QDate::currentDate().addYears( 20 ), QTime( 0, 0, 0 ) );
|
|
}
|
|
return dt;
|
|
}
|
|
|
|
static icaltimetype writeLocalICalDateTime( const QDateTime &utc, int offset )
|
|
{
|
|
QDateTime local = utc.addSecs( offset );
|
|
icaltimetype t = icaltime_null_time();
|
|
t.year = local.date().year();
|
|
t.month = local.date().month();
|
|
t.day = local.date().day();
|
|
t.hour = local.time().hour();
|
|
t.minute = local.time().minute();
|
|
t.second = local.time().second();
|
|
t.is_date = 0;
|
|
t.zone = 0;
|
|
t.is_utc = 0;
|
|
return t;
|
|
}
|
|
|
|
namespace KCal {
|
|
|
|
/******************************************************************************/
|
|
|
|
//@cond PRIVATE
|
|
class ICalTimeZonesPrivate
|
|
{
|
|
public:
|
|
ICalTimeZonesPrivate() {}
|
|
ICalTimeZones::ZoneMap zones;
|
|
};
|
|
//@endcond
|
|
|
|
ICalTimeZones::ICalTimeZones()
|
|
: d( new ICalTimeZonesPrivate )
|
|
{
|
|
}
|
|
|
|
ICalTimeZones::~ICalTimeZones()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
const ICalTimeZones::ZoneMap ICalTimeZones::zones() const
|
|
{
|
|
return d->zones;
|
|
}
|
|
|
|
bool ICalTimeZones::add( const ICalTimeZone &zone )
|
|
{
|
|
if ( !zone.isValid() ) {
|
|
return false;
|
|
}
|
|
if ( d->zones.find( zone.name() ) != d->zones.end() ) {
|
|
return false; // name already exists
|
|
}
|
|
|
|
d->zones.insert( zone.name(), zone );
|
|
return true;
|
|
}
|
|
|
|
ICalTimeZone ICalTimeZones::remove( const ICalTimeZone &zone )
|
|
{
|
|
if ( zone.isValid() ) {
|
|
for ( ZoneMap::Iterator it = d->zones.begin(), end = d->zones.end(); it != end; ++it ) {
|
|
if ( it.value() == zone ) {
|
|
d->zones.erase( it );
|
|
return ( zone == ICalTimeZone::utc() ) ? ICalTimeZone() : zone;
|
|
}
|
|
}
|
|
}
|
|
return ICalTimeZone();
|
|
}
|
|
|
|
ICalTimeZone ICalTimeZones::remove( const QString &name )
|
|
{
|
|
if ( !name.isEmpty() ) {
|
|
ZoneMap::Iterator it = d->zones.find( name );
|
|
if ( it != d->zones.end() ) {
|
|
ICalTimeZone zone = it.value();
|
|
d->zones.erase(it);
|
|
return ( zone == ICalTimeZone::utc() ) ? ICalTimeZone() : zone;
|
|
}
|
|
}
|
|
return ICalTimeZone();
|
|
}
|
|
|
|
void ICalTimeZones::clear()
|
|
{
|
|
d->zones.clear();
|
|
}
|
|
|
|
ICalTimeZone ICalTimeZones::zone( const QString &name ) const
|
|
{
|
|
if ( !name.isEmpty() ) {
|
|
ZoneMap::ConstIterator it = d->zones.constFind( name );
|
|
if ( it != d->zones.constEnd() ) {
|
|
return it.value();
|
|
}
|
|
}
|
|
return ICalTimeZone(); // error
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
ICalTimeZoneBackend::ICalTimeZoneBackend()
|
|
: KTimeZoneBackend()
|
|
{}
|
|
|
|
ICalTimeZoneBackend::ICalTimeZoneBackend( ICalTimeZoneSource *source,
|
|
const QString &name,
|
|
const QString &countryCode,
|
|
float latitude, float longitude,
|
|
const QString &comment )
|
|
: KTimeZoneBackend( source, name, countryCode, latitude, longitude, comment )
|
|
{}
|
|
|
|
ICalTimeZoneBackend::ICalTimeZoneBackend( const KTimeZone &tz, const QDate &earliest )
|
|
: KTimeZoneBackend( 0, tz.name(), tz.countryCode(), tz.latitude(), tz.longitude(), tz.comment() )
|
|
{
|
|
Q_UNUSED( earliest );
|
|
}
|
|
|
|
ICalTimeZoneBackend::~ICalTimeZoneBackend()
|
|
{}
|
|
|
|
KTimeZoneBackend *ICalTimeZoneBackend::clone() const
|
|
{
|
|
return new ICalTimeZoneBackend( *this );
|
|
}
|
|
|
|
QByteArray ICalTimeZoneBackend::type() const
|
|
{
|
|
return "ICalTimeZone";
|
|
}
|
|
|
|
bool ICalTimeZoneBackend::hasTransitions( const KTimeZone *caller ) const
|
|
{
|
|
Q_UNUSED( caller );
|
|
return true;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
ICalTimeZone::ICalTimeZone()
|
|
: KTimeZone( new ICalTimeZoneBackend() )
|
|
{}
|
|
|
|
ICalTimeZone::ICalTimeZone( ICalTimeZoneSource *source, const QString &name,
|
|
ICalTimeZoneData *data )
|
|
: KTimeZone( new ICalTimeZoneBackend( source, name ) )
|
|
{
|
|
setData( data );
|
|
}
|
|
|
|
ICalTimeZone::ICalTimeZone( const KTimeZone &tz, const QDate &earliest )
|
|
: KTimeZone( new ICalTimeZoneBackend( 0, tz.name(), tz.countryCode(),
|
|
tz.latitude(), tz.longitude(),
|
|
tz.comment() ) )
|
|
{
|
|
const KTimeZoneData *data = tz.data( true );
|
|
if ( data ) {
|
|
const ICalTimeZoneData *icaldata = dynamic_cast<const ICalTimeZoneData*>( data );
|
|
if ( icaldata ) {
|
|
setData( new ICalTimeZoneData( *icaldata ) );
|
|
} else {
|
|
setData( new ICalTimeZoneData( *data, tz, earliest ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
ICalTimeZone::~ICalTimeZone()
|
|
{}
|
|
|
|
QString ICalTimeZone::city() const
|
|
{
|
|
const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() );
|
|
return dat ? dat->city() : QString();
|
|
}
|
|
|
|
QByteArray ICalTimeZone::url() const
|
|
{
|
|
const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() );
|
|
return dat ? dat->url() : QByteArray();
|
|
}
|
|
|
|
QDateTime ICalTimeZone::lastModified() const
|
|
{
|
|
const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() );
|
|
return dat ? dat->lastModified() : QDateTime();
|
|
}
|
|
|
|
QByteArray ICalTimeZone::vtimezone() const
|
|
{
|
|
const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() );
|
|
return dat ? dat->vtimezone() : QByteArray();
|
|
}
|
|
|
|
icaltimezone *ICalTimeZone::icalTimezone() const
|
|
{
|
|
const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() );
|
|
return dat ? dat->icalTimezone() : 0;
|
|
}
|
|
|
|
bool ICalTimeZone::update( const ICalTimeZone &other )
|
|
{
|
|
if ( !updateBase( other ) ) {
|
|
return false;
|
|
}
|
|
|
|
KTimeZoneData *otherData = other.data() ? other.data()->clone() : 0;
|
|
setData( otherData, other.source() );
|
|
return true;
|
|
}
|
|
|
|
ICalTimeZone ICalTimeZone::utc()
|
|
{
|
|
static ICalTimeZone utcZone;
|
|
if ( !utcZone.isValid() ) {
|
|
ICalTimeZoneSource tzs;
|
|
utcZone = tzs.parse( icaltimezone_get_utc_timezone() );
|
|
}
|
|
return utcZone;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
//@cond PRIVATE
|
|
class ICalTimeZoneDataPrivate
|
|
{
|
|
public:
|
|
ICalTimeZoneDataPrivate() : icalComponent(0) {}
|
|
~ICalTimeZoneDataPrivate()
|
|
{
|
|
if ( icalComponent ) {
|
|
icalcomponent_free( icalComponent );
|
|
}
|
|
}
|
|
icalcomponent *component() const { return icalComponent; }
|
|
void setComponent( icalcomponent *c )
|
|
{
|
|
if ( icalComponent ) {
|
|
icalcomponent_free( icalComponent );
|
|
}
|
|
icalComponent = c;
|
|
}
|
|
QString location; // name of city for this time zone
|
|
QByteArray url; // URL of published VTIMEZONE definition (optional)
|
|
QDateTime lastModified; // time of last modification of the VTIMEZONE component (optional)
|
|
private:
|
|
icalcomponent *icalComponent; // ical component representing this time zone
|
|
};
|
|
//@endcond
|
|
|
|
ICalTimeZoneData::ICalTimeZoneData()
|
|
: d ( new ICalTimeZoneDataPrivate() )
|
|
{
|
|
}
|
|
|
|
ICalTimeZoneData::ICalTimeZoneData( const ICalTimeZoneData &rhs )
|
|
: KTimeZoneData( rhs ),
|
|
d( new ICalTimeZoneDataPrivate() )
|
|
{
|
|
d->location = rhs.d->location;
|
|
d->url = rhs.d->url;
|
|
d->lastModified = rhs.d->lastModified;
|
|
d->setComponent( icalcomponent_new_clone( rhs.d->component() ) );
|
|
}
|
|
|
|
ICalTimeZoneData::ICalTimeZoneData( const KTimeZoneData &rhs,
|
|
const KTimeZone &tz, const QDate &earliest )
|
|
: KTimeZoneData( rhs ),
|
|
d( new ICalTimeZoneDataPrivate() )
|
|
{
|
|
// VTIMEZONE RRULE types
|
|
enum {
|
|
DAY_OF_MONTH = 0x01,
|
|
WEEKDAY_OF_MONTH = 0x02,
|
|
LAST_WEEKDAY_OF_MONTH = 0x04
|
|
};
|
|
|
|
if ( tz.type() == "KSystemTimeZone" ) {
|
|
// Try to fetch a system time zone in preference, on the grounds
|
|
// that system time zones are more likely to be up to date than
|
|
// built-in libical ones.
|
|
icalcomponent *c = 0;
|
|
KTimeZone ktz = KSystemTimeZones::readZone( tz.name() );
|
|
if ( ktz.isValid() ) {
|
|
if ( ktz.data(true) ) {
|
|
ICalTimeZone icaltz( ktz, earliest );
|
|
icaltimezone *itz = icaltz.icalTimezone();
|
|
c = icalcomponent_new_clone( icaltimezone_get_component( itz ) );
|
|
icaltimezone_free( itz, 1 );
|
|
}
|
|
}
|
|
if ( !c ) {
|
|
// Try to fetch a built-in libical time zone.
|
|
icaltimezone *itz = icaltimezone_get_builtin_timezone( tz.name().toUtf8() );
|
|
c = icalcomponent_new_clone( icaltimezone_get_component( itz ) );
|
|
}
|
|
if ( c ) {
|
|
// TZID in built-in libical time zones has a standard prefix.
|
|
// To make the VTIMEZONE TZID match TZID references in incidences
|
|
// (as required by RFC2445), strip off the prefix.
|
|
icalproperty *prop = icalcomponent_get_first_property( c, ICAL_TZID_PROPERTY );
|
|
if ( prop ) {
|
|
icalvalue *value = icalproperty_get_value( prop );
|
|
const char *tzid = icalvalue_get_text( value );
|
|
QByteArray icalprefix = ICalTimeZoneSource::icalTzidPrefix();
|
|
int len = icalprefix.size();
|
|
if ( !strncmp( icalprefix, tzid, len ) ) {
|
|
const char *s = strchr( tzid + len, '/' ); // find third '/'
|
|
if ( s ) {
|
|
QByteArray tzidShort( s + 1 ); // deep copy of string (needed by icalvalue_set_text())
|
|
icalvalue_set_text( value, tzidShort );
|
|
|
|
// Remove the X-LIC-LOCATION property, which is only used by libical
|
|
prop = icalcomponent_get_first_property( c, ICAL_X_PROPERTY );
|
|
const char *xname = icalproperty_get_x_name( prop );
|
|
if ( xname && !strcmp( xname, "X-LIC-LOCATION" ) ) {
|
|
icalcomponent_remove_property( c, prop );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
d->setComponent( c );
|
|
} else {
|
|
// Write the time zone data into an iCal component
|
|
icalcomponent *tzcomp = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT);
|
|
icalcomponent_add_property( tzcomp, icalproperty_new_tzid( tz.name().toUtf8() ) );
|
|
// icalcomponent_add_property(tzcomp, icalproperty_new_location( tz.name().toUtf8() ));
|
|
|
|
// Compile an ordered list of transitions so that we can know the phases
|
|
// which occur before and after each transition.
|
|
QList<KTimeZone::Transition> transits = transitions();
|
|
if ( earliest.isValid() ) {
|
|
// Remove all transitions earlier than those we are interested in
|
|
for ( int i = 0, end = transits.count(); i < end; ++i ) {
|
|
if ( transits[i].time().date() >= earliest ) {
|
|
if ( i > 0 ) {
|
|
transits.erase( transits.begin(), transits.begin() + i );
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
int trcount = transits.count();
|
|
QVector<bool> transitionsDone(trcount);
|
|
transitionsDone.fill(false);
|
|
|
|
// Go through the list of transitions and create an iCal component for each
|
|
// distinct combination of phase after and UTC offset before the transition.
|
|
icaldatetimeperiodtype dtperiod;
|
|
dtperiod.period = icalperiodtype_null_period();
|
|
for ( ; ; ) {
|
|
int i = 0;
|
|
for ( ; i < trcount && transitionsDone[i]; ++i ) {
|
|
;
|
|
}
|
|
if ( i >= trcount ) {
|
|
break;
|
|
}
|
|
// Found a phase combination which hasn't yet been processed
|
|
int preOffset = ( i > 0 ) ? transits[i - 1].phase().utcOffset() : rhs.previousUtcOffset();
|
|
KTimeZone::Phase phase = transits[i].phase();
|
|
if ( phase.utcOffset() == preOffset ) {
|
|
transitionsDone[i] = true;
|
|
while ( ++i < trcount ) {
|
|
if ( transitionsDone[i] ||
|
|
transits[i].phase() != phase ||
|
|
transits[i - 1].phase().utcOffset() != preOffset ) {
|
|
continue;
|
|
}
|
|
transitionsDone[i] = true;
|
|
}
|
|
continue;
|
|
}
|
|
icalcomponent *phaseComp =
|
|
icalcomponent_new( phase.isDst() ? ICAL_XDAYLIGHT_COMPONENT : ICAL_XSTANDARD_COMPONENT );
|
|
QList<QByteArray> abbrevs = phase.abbreviations();
|
|
for ( int a = 0, aend = abbrevs.count(); a < aend; ++a ) {
|
|
icalcomponent_add_property( phaseComp,
|
|
icalproperty_new_tzname(
|
|
static_cast<const char*>( abbrevs[a]) ) );
|
|
}
|
|
if ( !phase.comment().isEmpty() ) {
|
|
icalcomponent_add_property( phaseComp,
|
|
icalproperty_new_comment( phase.comment().toUtf8() ) );
|
|
}
|
|
icalcomponent_add_property( phaseComp,
|
|
icalproperty_new_tzoffsetfrom( preOffset ) );
|
|
icalcomponent_add_property( phaseComp,
|
|
icalproperty_new_tzoffsetto( phase.utcOffset() ) );
|
|
// Create a component to hold initial RRULE if any, plus all RDATEs
|
|
icalcomponent *phaseComp1 = icalcomponent_new_clone( phaseComp );
|
|
icalcomponent_add_property( phaseComp1,
|
|
icalproperty_new_dtstart(
|
|
writeLocalICalDateTime( transits[i].time(), preOffset ) ) );
|
|
bool useNewRRULE = false;
|
|
|
|
// Compile the list of UTC transition dates/times, and check
|
|
// if the list can be reduced to an RRULE instead of multiple RDATEs.
|
|
QTime time;
|
|
QDate date;
|
|
int year = 0, month = 0, daysInMonth = 0, dayOfMonth = 0; // avoid compiler warnings
|
|
int dayOfWeek = 0; // Monday = 1
|
|
int nthFromStart = 0; // nth (weekday) of month
|
|
int nthFromEnd = 0; // nth last (weekday) of month
|
|
int newRule;
|
|
int rule = 0;
|
|
QList<QDateTime> rdates;// dates which (probably) need to be written as RDATEs
|
|
QList<QDateTime> times;
|
|
QDateTime qdt = transits[i].time(); // set 'qdt' for start of loop
|
|
times += qdt;
|
|
transitionsDone[i] = true;
|
|
do {
|
|
if ( !rule ) {
|
|
// Initialise data for detecting a new rule
|
|
rule = DAY_OF_MONTH | WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH;
|
|
time = qdt.time();
|
|
date = qdt.date();
|
|
year = date.year();
|
|
month = date.month();
|
|
daysInMonth = date.daysInMonth();
|
|
dayOfWeek = date.dayOfWeek(); // Monday = 1
|
|
dayOfMonth = date.day();
|
|
nthFromStart = ( dayOfMonth - 1 ) / 7 + 1; // nth (weekday) of month
|
|
nthFromEnd = ( daysInMonth - dayOfMonth ) / 7 + 1; // nth last (weekday) of month
|
|
}
|
|
if ( ++i >= trcount ) {
|
|
newRule = 0;
|
|
times += QDateTime(); // append a dummy value since last value in list is ignored
|
|
} else {
|
|
if ( transitionsDone[i] ||
|
|
transits[i].phase() != phase ||
|
|
transits[i - 1].phase().utcOffset() != preOffset ) {
|
|
continue;
|
|
}
|
|
transitionsDone[i] = true;
|
|
qdt = transits[i].time();
|
|
if ( !qdt.isValid() ) {
|
|
continue;
|
|
}
|
|
newRule = rule;
|
|
times += qdt;
|
|
date = qdt.date();
|
|
if ( qdt.time() != time ||
|
|
date.month() != month ||
|
|
date.year() != ++year ) {
|
|
newRule = 0;
|
|
} else {
|
|
int day = date.day();
|
|
if ( ( newRule & DAY_OF_MONTH ) && day != dayOfMonth ) {
|
|
newRule &= ~DAY_OF_MONTH;
|
|
}
|
|
if ( newRule & ( WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH ) ) {
|
|
if ( date.dayOfWeek() != dayOfWeek ) {
|
|
newRule &= ~( WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH );
|
|
} else {
|
|
if ( ( newRule & WEEKDAY_OF_MONTH ) &&
|
|
( day - 1 ) / 7 + 1 != nthFromStart ) {
|
|
newRule &= ~WEEKDAY_OF_MONTH;
|
|
}
|
|
if ( ( newRule & LAST_WEEKDAY_OF_MONTH ) &&
|
|
( daysInMonth - day ) / 7 + 1 != nthFromEnd ) {
|
|
newRule &= ~LAST_WEEKDAY_OF_MONTH;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ( !newRule ) {
|
|
// The previous rule (if any) no longer applies.
|
|
// Write all the times up to but not including the current one.
|
|
// First check whether any of the last RDATE values fit this rule.
|
|
int yr = times[0].date().year();
|
|
while ( !rdates.isEmpty() ) {
|
|
qdt = rdates.last();
|
|
date = qdt.date();
|
|
if ( qdt.time() != time ||
|
|
date.month() != month ||
|
|
date.year() != --yr ) {
|
|
break;
|
|
}
|
|
int day = date.day();
|
|
if ( rule & DAY_OF_MONTH ) {
|
|
if ( day != dayOfMonth ) {
|
|
break;
|
|
}
|
|
} else {
|
|
if ( date.dayOfWeek() != dayOfWeek ||
|
|
( ( rule & WEEKDAY_OF_MONTH ) &&
|
|
( day - 1 ) / 7 + 1 != nthFromStart ) ||
|
|
( ( rule & LAST_WEEKDAY_OF_MONTH ) &&
|
|
( daysInMonth - day ) / 7 + 1 != nthFromEnd ) ) {
|
|
break;
|
|
}
|
|
}
|
|
times.prepend( qdt );
|
|
rdates.pop_back();
|
|
}
|
|
if ( times.count() > ( useNewRRULE ? minPhaseCount : minRuleCount ) ) {
|
|
// There are enough dates to combine into an RRULE
|
|
icalrecurrencetype r;
|
|
icalrecurrencetype_clear( &r );
|
|
r.freq = ICAL_YEARLY_RECURRENCE;
|
|
r.count = ( year >= 2030 ) ? 0 : times.count() - 1;
|
|
r.by_month[0] = month;
|
|
if ( rule & DAY_OF_MONTH ) {
|
|
r.by_month_day[0] = dayOfMonth;
|
|
} else if ( rule & WEEKDAY_OF_MONTH ) {
|
|
r.by_day[0] = ( dayOfWeek % 7 + 1 ) + ( nthFromStart * 8 ); // Sunday = 1
|
|
} else if ( rule & LAST_WEEKDAY_OF_MONTH ) {
|
|
r.by_day[0] = -( dayOfWeek % 7 + 1 ) - ( nthFromEnd * 8 ); // Sunday = 1
|
|
}
|
|
icalproperty *prop = icalproperty_new_rrule( r );
|
|
if ( useNewRRULE ) {
|
|
// This RRULE doesn't start from the phase start date, so set it into
|
|
// a new STANDARD/DAYLIGHT component in the VTIMEZONE.
|
|
icalcomponent *c = icalcomponent_new_clone( phaseComp );
|
|
icalcomponent_add_property(
|
|
c, icalproperty_new_dtstart( writeLocalICalDateTime( times[0], preOffset ) ) );
|
|
icalcomponent_add_property( c, prop );
|
|
icalcomponent_add_component( tzcomp, c );
|
|
} else {
|
|
icalcomponent_add_property( phaseComp1, prop );
|
|
}
|
|
} else {
|
|
// Save dates for writing as RDATEs
|
|
for ( int t = 0, tend = times.count() - 1; t < tend; ++t ) {
|
|
rdates += times[t];
|
|
}
|
|
}
|
|
useNewRRULE = true;
|
|
// All date/time values but the last have been added to the VTIMEZONE.
|
|
// Remove them from the list.
|
|
qdt = times.last(); // set 'qdt' for start of loop
|
|
times.clear();
|
|
times += qdt;
|
|
}
|
|
rule = newRule;
|
|
} while ( i < trcount );
|
|
|
|
// Write remaining dates as RDATEs
|
|
for ( int rd = 0, rdend = rdates.count(); rd < rdend; ++rd ) {
|
|
dtperiod.time = writeLocalICalDateTime( rdates[rd], preOffset );
|
|
icalcomponent_add_property( phaseComp1, icalproperty_new_rdate( dtperiod ) );
|
|
}
|
|
icalcomponent_add_component( tzcomp, phaseComp1 );
|
|
icalcomponent_free( phaseComp );
|
|
}
|
|
|
|
d->setComponent( tzcomp );
|
|
}
|
|
}
|
|
|
|
ICalTimeZoneData::~ICalTimeZoneData()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
ICalTimeZoneData &ICalTimeZoneData::operator=( const ICalTimeZoneData &rhs )
|
|
{
|
|
// check for self assignment
|
|
if ( &rhs == this ) {
|
|
return *this;
|
|
}
|
|
|
|
KTimeZoneData::operator=( rhs );
|
|
d->location = rhs.d->location;
|
|
d->url = rhs.d->url;
|
|
d->lastModified = rhs.d->lastModified;
|
|
d->setComponent( icalcomponent_new_clone( rhs.d->component() ) );
|
|
return *this;
|
|
}
|
|
|
|
KTimeZoneData *ICalTimeZoneData::clone() const
|
|
{
|
|
return new ICalTimeZoneData( *this );
|
|
}
|
|
|
|
QString ICalTimeZoneData::city() const
|
|
{
|
|
return d->location;
|
|
}
|
|
|
|
QByteArray ICalTimeZoneData::url() const
|
|
{
|
|
return d->url;
|
|
}
|
|
|
|
QDateTime ICalTimeZoneData::lastModified() const
|
|
{
|
|
return d->lastModified;
|
|
}
|
|
|
|
QByteArray ICalTimeZoneData::vtimezone() const
|
|
{
|
|
QByteArray result( icalcomponent_as_ical_string( d->component() ) );
|
|
icalmemory_free_ring();
|
|
return result;
|
|
}
|
|
|
|
icaltimezone *ICalTimeZoneData::icalTimezone() const
|
|
{
|
|
icaltimezone *icaltz = icaltimezone_new();
|
|
if ( !icaltz ) {
|
|
return 0;
|
|
}
|
|
icalcomponent *c = icalcomponent_new_clone( d->component() );
|
|
if ( !icaltimezone_set_component( icaltz, c ) ) {
|
|
icalcomponent_free( c );
|
|
icaltimezone_free( icaltz, 1 );
|
|
return 0;
|
|
}
|
|
return icaltz;
|
|
}
|
|
|
|
bool ICalTimeZoneData::hasTransitions() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
//@cond PRIVATE
|
|
class ICalTimeZoneSourcePrivate
|
|
{
|
|
public:
|
|
static QList<QDateTime> parsePhase( icalcomponent *, bool daylight,
|
|
int &prevOffset, KTimeZone::Phase & );
|
|
static QByteArray icalTzidPrefix;
|
|
};
|
|
|
|
QByteArray ICalTimeZoneSourcePrivate::icalTzidPrefix;
|
|
//@endcond
|
|
|
|
ICalTimeZoneSource::ICalTimeZoneSource()
|
|
: KTimeZoneSource( false ),
|
|
d( 0 )
|
|
{
|
|
}
|
|
|
|
ICalTimeZoneSource::~ICalTimeZoneSource()
|
|
{
|
|
}
|
|
|
|
bool ICalTimeZoneSource::parse( const QString &fileName, ICalTimeZones &zones )
|
|
{
|
|
QFile file( fileName );
|
|
if ( !file.open( QIODevice::ReadOnly ) ) {
|
|
return false;
|
|
}
|
|
QTextStream ts( &file );
|
|
ts.setCodec( "ISO 8859-1" );
|
|
QByteArray text = ts.readAll().trimmed().toLatin1();
|
|
file.close();
|
|
|
|
bool result = false;
|
|
icalcomponent *calendar = icalcomponent_new_from_string( text.data() );
|
|
if ( calendar ) {
|
|
if ( icalcomponent_isa( calendar ) == ICAL_VCALENDAR_COMPONENT ) {
|
|
result = parse( calendar, zones );
|
|
}
|
|
icalcomponent_free( calendar );
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool ICalTimeZoneSource::parse( icalcomponent *calendar, ICalTimeZones &zones )
|
|
{
|
|
for ( icalcomponent *c = icalcomponent_get_first_component( calendar, ICAL_VTIMEZONE_COMPONENT );
|
|
c; c = icalcomponent_get_next_component( calendar, ICAL_VTIMEZONE_COMPONENT ) ) {
|
|
ICalTimeZone zone = parse( c );
|
|
if ( !zone.isValid() ) {
|
|
return false;
|
|
}
|
|
ICalTimeZone oldzone = zones.zone( zone.name() );
|
|
if ( oldzone.isValid() ) {
|
|
// The zone already exists in the collection, so update the definition
|
|
// of the zone rather than using a newly created one.
|
|
oldzone.update( zone );
|
|
} else if ( !zones.add( zone ) ) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
ICalTimeZone ICalTimeZoneSource::parse( icalcomponent *vtimezone )
|
|
{
|
|
QString name;
|
|
QString xlocation;
|
|
ICalTimeZoneData *data = new ICalTimeZoneData();
|
|
|
|
// Read the fixed properties which can only appear once in VTIMEZONE
|
|
icalproperty *p = icalcomponent_get_first_property( vtimezone, ICAL_ANY_PROPERTY );
|
|
while ( p ) {
|
|
icalproperty_kind kind = icalproperty_isa( p );
|
|
switch ( kind ) {
|
|
|
|
case ICAL_TZID_PROPERTY:
|
|
name = QString::fromUtf8( icalproperty_get_tzid( p ) );
|
|
break;
|
|
|
|
case ICAL_TZURL_PROPERTY:
|
|
data->d->url = icalproperty_get_tzurl( p );
|
|
break;
|
|
|
|
case ICAL_LOCATION_PROPERTY:
|
|
// This isn't mentioned in RFC2445, but libical reads it ...
|
|
data->d->location = QString::fromUtf8( icalproperty_get_location( p ) );
|
|
break;
|
|
|
|
case ICAL_X_PROPERTY:
|
|
{ // use X-LIC-LOCATION if LOCATION is missing
|
|
const char *xname = icalproperty_get_x_name( p );
|
|
if ( xname && !strcmp( xname, "X-LIC-LOCATION" ) ) {
|
|
xlocation = QString::fromUtf8( icalproperty_get_x( p ) );
|
|
}
|
|
break;
|
|
}
|
|
case ICAL_LASTMODIFIED_PROPERTY:
|
|
{
|
|
icaltimetype t = icalproperty_get_lastmodified(p);
|
|
if ( t.is_utc ) {
|
|
data->d->lastModified = toQDateTime( t );
|
|
} else {
|
|
kDebug() << "LAST-MODIFIED not UTC";
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
p = icalcomponent_get_next_property( vtimezone, ICAL_ANY_PROPERTY );
|
|
}
|
|
|
|
if ( name.isEmpty() ) {
|
|
kDebug() << "TZID missing";
|
|
delete data;
|
|
return ICalTimeZone();
|
|
}
|
|
if ( data->d->location.isEmpty() && !xlocation.isEmpty() ) {
|
|
data->d->location = xlocation;
|
|
}
|
|
QString prefix = QString::fromUtf8( icalTzidPrefix() );
|
|
if ( name.startsWith( prefix ) ) {
|
|
// Remove the prefix from libical built in time zone TZID
|
|
int i = name.indexOf( '/', prefix.length() );
|
|
if ( i > 0 ) {
|
|
name = name.mid( i + 1 );
|
|
}
|
|
}
|
|
//kDebug() << "---zoneId: \"" << name << '"';
|
|
|
|
/*
|
|
* Iterate through all time zone rules for this VTIMEZONE,
|
|
* and create a Phase object containing details for each one.
|
|
*/
|
|
int prevOffset = 0;
|
|
QList<KTimeZone::Transition> transitions;
|
|
QDateTime earliest;
|
|
QList<KTimeZone::Phase> phases;
|
|
for ( icalcomponent *c = icalcomponent_get_first_component( vtimezone, ICAL_ANY_COMPONENT );
|
|
c; c = icalcomponent_get_next_component( vtimezone, ICAL_ANY_COMPONENT ) )
|
|
{
|
|
int prevoff = 0;
|
|
KTimeZone::Phase phase;
|
|
QList<QDateTime> times;
|
|
icalcomponent_kind kind = icalcomponent_isa( c );
|
|
switch ( kind ) {
|
|
|
|
case ICAL_XSTANDARD_COMPONENT:
|
|
//kDebug() << "---standard phase: found";
|
|
times = ICalTimeZoneSourcePrivate::parsePhase( c, false, prevoff, phase );
|
|
break;
|
|
|
|
case ICAL_XDAYLIGHT_COMPONENT:
|
|
//kDebug() << "---daylight phase: found";
|
|
times = ICalTimeZoneSourcePrivate::parsePhase( c, true, prevoff, phase );
|
|
break;
|
|
|
|
default:
|
|
kDebug() << "Unknown component:" << kind;
|
|
break;
|
|
}
|
|
int tcount = times.count();
|
|
if ( tcount ) {
|
|
phases += phase;
|
|
for ( int t = 0; t < tcount; ++t ) {
|
|
transitions += KTimeZone::Transition( times[t], phase );
|
|
}
|
|
if ( !earliest.isValid() || times[0] < earliest ) {
|
|
prevOffset = prevoff;
|
|
earliest = times[0];
|
|
}
|
|
}
|
|
}
|
|
data->setPhases( phases, prevOffset );
|
|
// Remove any "duplicate" transitions, i.e. those where two consecutive
|
|
// transitions have the same phase.
|
|
qSort( transitions );
|
|
for ( int t = 1, tend = transitions.count(); t < tend; ) {
|
|
if ( transitions[t].phase() == transitions[t - 1].phase() ) {
|
|
transitions.removeAt( t );
|
|
--tend;
|
|
} else {
|
|
++t;
|
|
}
|
|
}
|
|
data->setTransitions( transitions );
|
|
|
|
data->d->setComponent( icalcomponent_new_clone( vtimezone ) );
|
|
kDebug() << "VTIMEZONE" << name;
|
|
return ICalTimeZone( this, name, data );
|
|
}
|
|
|
|
ICalTimeZone ICalTimeZoneSource::parse( icaltimezone *tz )
|
|
{
|
|
/* Parse the VTIMEZONE component stored in the icaltimezone structure.
|
|
* This is both easier and provides more complete information than
|
|
* extracting already parsed data from icaltimezone.
|
|
*/
|
|
return tz ? parse( icaltimezone_get_component( tz ) ) : ICalTimeZone();
|
|
}
|
|
|
|
//@cond PRIVATE
|
|
QList<QDateTime> ICalTimeZoneSourcePrivate::parsePhase( icalcomponent *c,
|
|
bool daylight,
|
|
int &prevOffset,
|
|
KTimeZone::Phase &phase )
|
|
{
|
|
QList<QDateTime> transitions;
|
|
|
|
// Read the observance data for this standard/daylight savings phase
|
|
QList<QByteArray> abbrevs;
|
|
QString comment;
|
|
prevOffset = 0;
|
|
int utcOffset = 0;
|
|
bool recurs = false;
|
|
bool found_dtstart = false;
|
|
bool found_tzoffsetfrom = false;
|
|
bool found_tzoffsetto = false;
|
|
icaltimetype dtstart = icaltime_null_time();
|
|
|
|
// Now do the ical reading.
|
|
icalproperty *p = icalcomponent_get_first_property( c, ICAL_ANY_PROPERTY );
|
|
while ( p ) {
|
|
icalproperty_kind kind = icalproperty_isa( p );
|
|
switch ( kind ) {
|
|
|
|
case ICAL_TZNAME_PROPERTY: // abbreviated name for this time offset
|
|
{
|
|
// TZNAME can appear multiple times in order to provide language
|
|
// translations of the time zone offset name.
|
|
|
|
// TODO: Does this cope with multiple language specifications?
|
|
QByteArray tzname = icalproperty_get_tzname( p );
|
|
// Outlook (2000) places "Standard Time" and "Daylight Time" in the TZNAME
|
|
// strings, which is totally useless. So ignore those.
|
|
if ( ( !daylight && tzname == "Standard Time" ) ||
|
|
( daylight && tzname == "Daylight Time" ) ) {
|
|
break;
|
|
}
|
|
if ( !abbrevs.contains( tzname ) ) {
|
|
abbrevs += tzname;
|
|
}
|
|
break;
|
|
}
|
|
case ICAL_DTSTART_PROPERTY: // local time at which phase starts
|
|
dtstart = icalproperty_get_dtstart( p );
|
|
found_dtstart = true;
|
|
break;
|
|
|
|
case ICAL_TZOFFSETFROM_PROPERTY: // UTC offset immediately before start of phase
|
|
prevOffset = icalproperty_get_tzoffsetfrom( p );
|
|
found_tzoffsetfrom = true;
|
|
break;
|
|
|
|
case ICAL_TZOFFSETTO_PROPERTY:
|
|
utcOffset = icalproperty_get_tzoffsetto( p );
|
|
found_tzoffsetto = true;
|
|
break;
|
|
|
|
case ICAL_COMMENT_PROPERTY:
|
|
comment = QString::fromUtf8( icalproperty_get_comment( p ) );
|
|
break;
|
|
|
|
case ICAL_RDATE_PROPERTY:
|
|
case ICAL_RRULE_PROPERTY:
|
|
recurs = true;
|
|
break;
|
|
|
|
default:
|
|
kDebug() << "Unknown property:" << kind;
|
|
break;
|
|
}
|
|
p = icalcomponent_get_next_property( c, ICAL_ANY_PROPERTY );
|
|
}
|
|
|
|
// Validate the phase data
|
|
if ( !found_dtstart || !found_tzoffsetfrom || !found_tzoffsetto ) {
|
|
kDebug() << "DTSTART/TZOFFSETFROM/TZOFFSETTO missing";
|
|
return transitions;
|
|
}
|
|
|
|
// Convert DTSTART to QDateTime, and from local time to UTC
|
|
QDateTime localStart = toQDateTime( dtstart ); // local time
|
|
dtstart.second -= prevOffset;
|
|
dtstart.is_utc = 1;
|
|
QDateTime utcStart = toQDateTime( icaltime_normalize( dtstart ) ); // UTC
|
|
|
|
transitions += utcStart;
|
|
if ( recurs ) {
|
|
/* RDATE or RRULE is specified. There should only be one or the other, but
|
|
* it doesn't really matter - the code can cope with both.
|
|
* Note that we had to get DTSTART, TZOFFSETFROM, TZOFFSETTO before reading
|
|
* recurrences.
|
|
*/
|
|
KDateTime klocalStart( localStart, KDateTime::Spec::ClockTime() );
|
|
KDateTime maxTime( MAX_DATE(), KDateTime::Spec::ClockTime() );
|
|
Recurrence recur;
|
|
icalproperty *p = icalcomponent_get_first_property( c, ICAL_ANY_PROPERTY );
|
|
while ( p ) {
|
|
icalproperty_kind kind = icalproperty_isa( p );
|
|
switch ( kind ) {
|
|
|
|
case ICAL_RDATE_PROPERTY:
|
|
{
|
|
icaltimetype t = icalproperty_get_rdate(p).time;
|
|
if ( icaltime_is_date( t ) ) {
|
|
// RDATE with a DATE value inherits the (local) time from DTSTART
|
|
t.hour = dtstart.hour;
|
|
t.minute = dtstart.minute;
|
|
t.second = dtstart.second;
|
|
t.is_date = 0;
|
|
t.is_utc = 0; // dtstart is in local time
|
|
}
|
|
// RFC2445 states that RDATE must be in local time,
|
|
// but we support UTC as well to be safe.
|
|
if ( !t.is_utc ) {
|
|
t.second -= prevOffset; // convert to UTC
|
|
t.is_utc = 1;
|
|
t = icaltime_normalize( t );
|
|
}
|
|
transitions += toQDateTime( t );
|
|
break;
|
|
}
|
|
case ICAL_RRULE_PROPERTY:
|
|
{
|
|
RecurrenceRule r;
|
|
ICalFormat icf;
|
|
ICalFormatImpl impl( &icf );
|
|
impl.readRecurrence( icalproperty_get_rrule( p ), &r );
|
|
r.setStartDt( klocalStart );
|
|
// The end date time specified in an RRULE should be in UTC.
|
|
// Convert to local time to avoid timesInInterval() getting things wrong.
|
|
if ( r.duration() == 0 ) {
|
|
KDateTime end( r.endDt() );
|
|
if ( end.timeSpec() == KDateTime::Spec::UTC() ) {
|
|
end.setTimeSpec( KDateTime::Spec::ClockTime() );
|
|
r.setEndDt( end.addSecs( prevOffset ) );
|
|
}
|
|
}
|
|
DateTimeList dts = r.timesInInterval( klocalStart, maxTime );
|
|
for ( int i = 0, end = dts.count(); i < end; ++i ) {
|
|
QDateTime utc = dts[i].dateTime();
|
|
utc.setTimeSpec( Qt::UTC );
|
|
transitions += utc.addSecs( -prevOffset );
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
p = icalcomponent_get_next_property( c, ICAL_ANY_PROPERTY );
|
|
}
|
|
qSortUnique( transitions );
|
|
}
|
|
|
|
phase = KTimeZone::Phase( utcOffset, abbrevs, daylight, comment );
|
|
return transitions;
|
|
}
|
|
//@endcond
|
|
|
|
ICalTimeZone ICalTimeZoneSource::standardZone( const QString &zone, bool icalBuiltIn )
|
|
{
|
|
if ( !icalBuiltIn ) {
|
|
// Try to fetch a system time zone in preference, on the grounds
|
|
// that system time zones are more likely to be up to date than
|
|
// built-in libical ones.
|
|
QString tzid = zone;
|
|
QString prefix = QString::fromUtf8( icalTzidPrefix() );
|
|
if ( zone.startsWith( prefix ) ) {
|
|
int i = zone.indexOf( '/', prefix.length() );
|
|
if ( i > 0 ) {
|
|
tzid = zone.mid( i + 1 ); // strip off the libical prefix
|
|
}
|
|
}
|
|
KTimeZone ktz = KSystemTimeZones::readZone( tzid );
|
|
if ( ktz.isValid() ) {
|
|
if ( ktz.data( true ) ) {
|
|
ICalTimeZone icaltz( ktz );
|
|
//kDebug() << zone << " read from system database";
|
|
return icaltz;
|
|
}
|
|
}
|
|
}
|
|
// Try to fetch a built-in libical time zone.
|
|
// First try to look it up as a geographical location (e.g. Europe/London)
|
|
QByteArray zoneName = zone.toUtf8();
|
|
icaltimezone *icaltz = icaltimezone_get_builtin_timezone( zoneName );
|
|
if ( !icaltz ) {
|
|
// This will find it if it includes the libical prefix
|
|
icaltz = icaltimezone_get_builtin_timezone_from_tzid( zoneName );
|
|
if ( !icaltz ) {
|
|
return ICalTimeZone();
|
|
}
|
|
}
|
|
return parse( icaltz );
|
|
}
|
|
|
|
QByteArray ICalTimeZoneSource::icalTzidPrefix()
|
|
{
|
|
if ( ICalTimeZoneSourcePrivate::icalTzidPrefix.isEmpty() ) {
|
|
icaltimezone *icaltz = icaltimezone_get_builtin_timezone( "Europe/London" );
|
|
QByteArray tzid = icaltimezone_get_tzid( icaltz );
|
|
if ( tzid.right( 13 ) == "Europe/London" ) {
|
|
int i = tzid.indexOf( '/', 1 );
|
|
if ( i > 0 ) {
|
|
ICalTimeZoneSourcePrivate::icalTzidPrefix = tzid.left( i + 1 );
|
|
return ICalTimeZoneSourcePrivate::icalTzidPrefix;
|
|
}
|
|
}
|
|
kError() << "failed to get libical TZID prefix";
|
|
}
|
|
return ICalTimeZoneSourcePrivate::icalTzidPrefix;
|
|
}
|
|
|
|
} // namespace KCal
|