Merge branch 'JJJollyjim/davical-refactor-freebusy'

This commit is contained in:
Andrew Ruthven 2019-01-04 22:37:25 +13:00
commit bcdf59ae2e
18 changed files with 224 additions and 35 deletions

View File

@ -29,6 +29,7 @@ test:
artifacts:
paths:
- testing/report.xml
- apache2_log/*
reports:
junit: testing/report.xml
when:
@ -68,4 +69,5 @@ test:
- apache2ctl start
- useradd testrunner
- cd testing && su testrunner -c 'IS_CI=yes ALLSUITES="regression-suite binding carddav scheduling" ./run_regressions.sh all x'
after_script:
- cp -r /var/log/apache2 apache2_log

View File

@ -167,8 +167,8 @@ CREATE TABLE calendar_item (
completed TIMESTAMP WITH TIME ZONE,
dav_id INT8 UNIQUE,
collection_id INT8 REFERENCES collection(collection_id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE,
first_instance_start TIMESTAMP WITHOUT TIME ZONE DEFAULT NULL,
last_instance_end TIMESTAMP WITHOUT TIME ZONE DEFAULT NULL,
first_instance_start TIMESTAMP WITH TIME ZONE DEFAULT NULL,
last_instance_end TIMESTAMP WITH TIME ZONE DEFAULT NULL,
-- Cascade updates / deletes from the caldav_data table
CONSTRAINT caldav_exists FOREIGN KEY ( user_no, dav_name )
@ -489,4 +489,4 @@ CREATE SEQUENCE metrics_count_delticket;
CREATE SEQUENCE metrics_count_bind;
CREATE SEQUENCE metrics_count_unknown;
SELECT new_db_revision(1,3,2, 'Luty' );
SELECT new_db_revision(1,3,3, 'Marzec' );

23
dba/patches/1.3.3.sql Normal file
View File

@ -0,0 +1,23 @@
-- Notable enhancement: change first_instance_start and last_instance_end to be timezone-aware
BEGIN;
SELECT check_db_revision(1,3,2);
ALTER TABLE calendar_item
ALTER COLUMN first_instance_start
TYPE TIMESTAMP WITH TIME ZONE
-- I don't believe the column has ever been populated, but if it has, we want
-- to throw away that data anyway because we don't know its timezone
USING NULL::timestamptz;
ALTER TABLE calendar_item
ALTER COLUMN last_instance_end
TYPE TIMESTAMP WITH TIME ZONE
USING NULL::timestamptz; -- As above
-- http://blogs.transparent.com/polish/names-of-the-months-and-their-meaning/
SELECT new_db_revision(1,3,3, 'Marzec' );
COMMIT;
ROLLBACK;

View File

@ -1295,6 +1295,15 @@ EOQRY;
}
/**
* Returns the name of the timezone for this collection, or the collection containing this resource
*/
function timezone_name() {
if ( !isset($this->collection) ) $this->FetchCollection();
return $this->collection->timezone;
}
/**
* Returns the database row for this resource
*/

View File

@ -305,6 +305,18 @@ class RepeatRuleDateTime extends DateTime {
return $this;
}
public static function withFallbackTzid( $date, $fallback_tzid ) {
// Floating times or dates (either VALUE=DATE or with no TZID) can default to the collection's tzid, if one is set
if ($date->GetParameterValue('VALUE') == 'DATE' && isset($fallback_tzid)) {
return new RepeatRuleDateTime($date->Value()."T000000", new RepeatRuleTimeZone($fallback_tzid));
} else if ($date->GetParameterValue('TZID') === null && isset($fallback_tzid)) {
return new RepeatRuleDateTime($date->Value(), new RepeatRuleTimeZone($fallback_tzid));
} else {
return new RepeatRuleDateTime($date);
}
}
public function __toString() {
return (string)parent::format(self::$Format) . ' ' . parent::getTimeZone()->getName();
@ -1144,7 +1156,7 @@ function rdate_expand( $dtstart, $property, $component, $range_end = null, $is_d
*
* @return array An array keyed on the UTC dates, referring to the component
*/
function rrule_expand( $dtstart, $property, $component, $range_end, $is_date=null, $return_floating_times=false ) {
function rrule_expand( $dtstart, $property, $component, $range_end, $is_date=null, $return_floating_times=false, $fallback_tzid=null ) {
$expansion = array();
$recur = $component->GetProperty($property);
@ -1153,7 +1165,7 @@ function rrule_expand( $dtstart, $property, $component, $range_end, $is_date=nul
$this_start = $component->GetProperty('DTSTART');
if ( isset($this_start) ) {
$this_start = new RepeatRuleDateTime($this_start);
$this_start = RepeatRuleDateTime::withFallbackTzid($this_start, $fallback_tzid);
}
else {
$this_start = clone($dtstart);
@ -1406,12 +1418,12 @@ function expand_event_instances( vComponent $vResource, $range_start = null, $ra
* @throws Exception (1) When DTSTART is not present but the RFC says MUST and (2) when we get an unsupported component
* @return RepeatRuleDateRange
*/
function getComponentRange(vComponent $comp) {
function getComponentRange(vComponent $comp, string $fallback_tzid = null) {
$dtstart_prop = $comp->GetProperty('DTSTART');
$duration_prop = $comp->GetProperty('DURATION');
if ( isset($duration_prop) ) {
if ( !isset($dtstart_prop) ) throw new Exception('Invalid '.$comp->GetType().' containing DURATION without DTSTART', 0);
$dtstart = new RepeatRuleDateTime($dtstart_prop);
$dtstart = RepeatRuleDateTime::withFallbackTzid($dtstart_prop, $fallback_tzid);
$dtend = clone($dtstart);
$dtend->modify(new Rfc5545Duration($duration_prop->Value()));
}
@ -1435,17 +1447,17 @@ function getComponentRange(vComponent $comp) {
}
if ( isset($dtstart_prop) )
$dtstart = new RepeatRuleDateTime($dtstart_prop);
$dtstart = RepeatRuleDateTime::withFallbackTzid($dtstart_prop, $fallback_tzid);
else
$dtstart = null;
if ( isset($dtend_prop) )
$dtend = new RepeatRuleDateTime($dtend_prop);
$dtend = RepeatRuleDateTime::withFallbackTzid($dtend_prop, $fallback_tzid);
else
$dtend = null;
if ( isset($completed_prop) ) {
$completed = new RepeatRuleDateTime($completed_prop);
$completed = RepeatRuleDateTime::withFallbackTzid($completed_prop, $fallback_tzid);
if ( !isset($dtstart) || (isset($dtstart) && $completed < $dtstart) ) $dtstart = $completed;
if ( !isset($dtend) || (isset($dtend) && $completed > $dtend) ) $dtend = $completed;
}
@ -1461,7 +1473,7 @@ function getComponentRange(vComponent $comp) {
* @param object $vResource A vComponent which is a VCALENDAR containing components needing expansion
* @return RepeatRuleDateRange Representing the range of time covered by the event.
*/
function getVCalendarRange( $vResource ) {
function getVCalendarRange( $vResource, string $fallback_tzid = null ) {
$components = $vResource->GetComponents();
$dtstart = null;
@ -1471,7 +1483,7 @@ function getVCalendarRange( $vResource ) {
$has_repeats = false;
foreach( $components AS $k => $comp ) {
if ( $comp->GetType() == 'VTIMEZONE' ) continue;
$range = getComponentRange($comp);
$range = getComponentRange($comp, $fallback_tzid);
$dtstart = $range->from;
if ( !isset($dtstart) ) continue;
$duration = $range->getDuration();
@ -1490,7 +1502,7 @@ function getVCalendarRange( $vResource ) {
$range_end = new RepeatRuleDateTime();
$range_end->modify('+150 years');
}
$instances += rrule_expand($dtstart, 'RRULE', $comp, $range_end);
$instances += rrule_expand($dtstart, 'RRULE', $comp, $range_end, null, false, $fallback_tzid);
$instances += rdate_expand($dtstart, 'RDATE', $comp, $range_end);
foreach ( rdate_expand($dtstart, 'EXDATE', $comp, $range_end) AS $k => $v ) {
unset($instances[$k]);

View File

@ -219,16 +219,23 @@ class WritableCollection extends DAVResource {
$calitem_params[':due'] = $first->GetPValue('DUE');
$calitem_params[':percent_complete'] = $first->GetPValue('PERCENT-COMPLETE');
$calitem_params[':status'] = $first->GetPValue('STATUS');
$range = getVCalendarRange($vcal, $this->timezone_name());
$calitem_params[':first_instance_start'] = isset($range->from) ? $range->from->UTC() : null;
$calitem_params[':last_instance_end'] = isset($range->until) ? $range->until->UTC() : null;
if ( $create_resource ) {
$sql = <<<EOSQL
INSERT INTO calendar_item (user_no, dav_name, dav_id, dav_etag, uid, dtstamp,
dtstart, dtend, summary, location, class, transp,
description, rrule, tz_id, last_modified, url, priority,
created, due, percent_complete, status, collection_id )
created, due, percent_complete, status, collection_id,
first_instance_start, last_instance_end )
VALUES ( :user_no, :dav_name, currval('dav_id_seq'), :etag, :uid, :dtstamp,
:dtstart, $dtend, :summary, :location, :class, :transp,
:description, :rrule, :tzid, :modified, :url, :priority,
:created, :due, :percent_complete, :status, $collection_id )
:created, :due, :percent_complete, :status, $collection_id,
:first_instance_start, :last_instance_end)
EOSQL;
$sync_change = 201;
}
@ -237,7 +244,8 @@ EOSQL;
UPDATE calendar_item SET dav_etag=:etag, uid=:uid, dtstamp=:dtstamp,
dtstart=:dtstart, dtend=$dtend, summary=:summary, location=:location, class=:class, transp=:transp,
description=:description, rrule=:rrule, tz_id=:tzid, last_modified=:modified, url=:url, priority=:priority,
created=:created, due=:due, percent_complete=:percent_complete, status=:status
created=:created, due=:due, percent_complete=:percent_complete, status=:status,
first_instance_start=:first_instance_start, last_instance_end=:last_instance_end
WHERE user_no=:user_no AND dav_name=:dav_name
EOSQL;
$sync_change = 200;

View File

@ -1215,6 +1215,10 @@ EOSQL;
$calitem_params[':percent_complete'] = $first->GetPValue('PERCENT-COMPLETE');
$calitem_params[':status'] = $first->GetPValue('STATUS');
// Intentionally not populating first_instance_start and last_instance_end
// here, As they may depend on the default tzid of the collection, which as
// I understand it hasn't yet been set
if ( $inserting ) {
$created = $first->GetPValue('CREATED');
if ( $created == '00001231T000000Z' ) $created = '20001231T000000Z';
@ -1596,7 +1600,11 @@ function write_resource( DAVResource $resource, $caldav_data, DAVResource $colle
$calitem_params[':percent_complete'] = $first->GetPValue('PERCENT-COMPLETE');
$calitem_params[':status'] = $first->GetPValue('STATUS');
// force re-render the object (to get the same representation for all attendiees)
$range = getVCalendarRange($vcal, $resource->timezone_name());
$calitem_params[':first_instance_start'] = isset($range->from) ? $range->from->UTC() : null;
$calitem_params[':last_instance_end'] = isset($range->until) ? $range->until->UTC() : null;
// force re-render the object (to get the same representation for all attendees)
$vcal->Render(null, true);
if ( !$collection->IsSchedulingCollection() ) {
@ -1632,11 +1640,13 @@ function write_resource( DAVResource $resource, $caldav_data, DAVResource $colle
INSERT INTO calendar_item (user_no, dav_name, dav_id, dav_etag, uid, dtstamp,
dtstart, dtend, summary, location, class, transp,
description, rrule, tz_id, last_modified, url, priority,
created, due, percent_complete, status, collection_id )
created, due, percent_complete, status, collection_id,
first_instance_start, last_instance_end )
VALUES ( :user_no, :dav_name, :dav_id, :etag, :uid, :dtstamp,
:dtstart, $dtend, :summary, :location, :class, :transp,
:description, :rrule, :tzid, :modified, :url, :priority,
:created, :due, :percent_complete, :status, :collection_id )
:created, :due, :percent_complete, :status, $collection_id,
:first_instance_start, :last_instance_end)
EOSQL;
$sync_change = 201;
$calitem_params[':collection_id'] = $collection_id;
@ -1650,7 +1660,8 @@ UPDATE calendar_item SET dav_etag=:etag, uid=:uid, dtstamp=:dtstamp,
dtstart=:dtstart, dtend=$dtend, summary=:summary, location=:location,
class=:class, transp=:transp, description=:description, rrule=:rrule,
tz_id=:tzid, last_modified=:modified, url=:url, priority=:priority,
due=:due, percent_complete=:percent_complete, status=:status
due=:due, percent_complete=:percent_complete, status=:status,
first_instance_start=:first_instance_start, last_instance_end=:last_instance_end
WHERE dav_id=:dav_id
EOSQL;
$sync_change = 200;

View File

@ -20,7 +20,10 @@ function get_freebusy( $path_match, $range_start, $range_end, $bin_privs = null
}
$params = array( ':path_match' => $path_match, ':start' => $range_start->UTC(), ':end' => $range_end->UTC() );
$where = ' WHERE caldav_data.dav_name ~ :path_match ';
$where .= 'AND rrule_event_overlaps( dtstart, dtend, rrule, :start, :end) ';
$where .= "AND (";
$where .= " (calendar_item.first_instance_start <= :end AND (:start <= calendar_item.last_instance_end OR calendar_item.last_instance_end IS NULL)) ";
$where .= " OR (calendar_item.first_instance_start IS NULL AND rrule_event_overlaps( dtstart, dtend, rrule, :start, :end)) ";
$where .= ") ";
$where .= "AND caldav_data.caldav_type IN ( 'VEVENT', 'VTODO' ) ";
$where .= "AND (calendar_item.transp != 'TRANSPARENT' OR calendar_item.transp IS NULL) ";
$where .= "AND (calendar_item.status != 'CANCELLED' OR calendar_item.status IS NULL) ";
@ -66,13 +69,7 @@ function get_freebusy( $path_match, $range_start, $range_end, $bin_privs = null
}
// Floating dtstarts (either VALUE=DATE or with no TZID) can default to the collection's tzid, if one is set
if ($start_date->GetParameterValue('VALUE') == 'DATE' && isset($collection_tzid)) {
$start_date = new RepeatRuleDateTime($start_date->Value()."T000000", new RepeatRuleTimeZone($collection_tzid));
} else if ($start_date->GetParameterValue('TZID') === null && isset($collection_tzid)) {
$start_date = new RepeatRuleDateTime($start_date->Value(), new RepeatRuleTimeZone($collection_tzid));
} else {
$start_date = new RepeatRuleDateTime($start_date);
}
$start_date = RepeatRuleDateTime::withFallbackTzid( $start_date, $collection_tzid );
$duration = $v->GetProperty('DURATION');
$duration = ( !isset($duration) ? 'P1D' : $duration->Value());

View File

@ -0,0 +1,119 @@
<?php
set_include_path(get_include_path() . PATH_SEPARATOR . '/usr/share/awl/inc' . PATH_SEPARATOR . 'inc');
require_once('RRule.php');
require_once('vCalendar.php');
use PHPUnit\Framework\TestCase;
final class RangeTest extends TestCase
{
public function testGetVCalendarRange() {
$cal = new vCalendar("BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Asia/Baku
BEGIN:STANDARD
TZOFFSETFROM:+0400
TZOFFSETTO:+0400
TZNAME:+04
DTSTART:19700101T000000
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20181218T045608Z
LAST-MODIFIED:20181218T045951Z
DTSTAMP:20181218T045951Z
UID:8eeee169-420f-4645-9546-9ea8293a1c6d
SUMMARY:Blep
RRULE:FREQ=DAILY;UNTIL=20190102T050000Z;BYDAY=MO,TU,WE,TH,FR
DTSTART;TZID=Asia/Baku:20181226T090000
DTEND;TZID=Asia/Baku:20181226T112000
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR");
$range = getVCalendarRange($cal);
self::assertEquals("20181226T050000Z", (string) $range->from->UTC());
self::assertEquals("20190102T072000Z", (string) $range->until->UTC());
// TZ is specified in the event, so this should be unaffected by passing in a fallback timezone:
$range = getVCalendarRange($cal, "Asia/Baku");
self::assertEquals("20181226T050000Z", (string) $range->from->UTC());
self::assertEquals("20190102T072000Z", (string) $range->until->UTC());
}
public function testGetVCalendarRangeTwoDayAllDay() {
$cal = new vCalendar("BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VEVENT
CREATED:20181218T052603Z
LAST-MODIFIED:20181218T052734Z
DTSTAMP:20181218T052734Z
UID:7e76efc0-b1ed-4b68-b0ed-9e34889c30bd
SUMMARY:New Event
RRULE:FREQ=WEEKLY;COUNT=3;BYDAY=TU
DTSTART;VALUE=DATE:20181225
DTEND;VALUE=DATE:20181227
TRANSP:TRANSPARENT
END:VEVENT
END:VCALENDAR");
$range = getVCalendarRange($cal, "Asia/Baku");
self::assertEquals("20181224T200000Z", (string) $range->from->UTC());
self::assertEquals("20190109T200000Z", (string) $range->until->UTC());
}
public function testGetVCalendarRangeFloating() {
// When interpreted as being in Greece, this event crosses the daylight savings boundary!
// TODO deal with how that affects all-day events...
$cal = new vCalendar("BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VEVENT
CREATED:20181218T052603Z
LAST-MODIFIED:20181218T052734Z
DTSTAMP:20181218T052734Z
UID:7e76efc0-b1ed-4b68-b0ed-9e34889c30bd
SUMMARY:New Event
RRULE:FREQ=MONTHLY;COUNT=4;INTERVAL=2;BYMONTHDAY=10
DTSTART:20180411T140000
DTEND:20180411T150000
TRANSP:TRANSPARENT
END:VEVENT
END:VCALENDAR");
$range = getVCalendarRange($cal, "Europe/Athens");
self::assertEquals("20180411T110000Z", (string) $range->from->UTC());
self::assertEquals("20181210T130000Z", (string) $range->until->UTC());
}
public function testGetVCalendarRangeAllDayAcrossDST() {
// When interpreted as being in Greece, this event crosses the daylight savings boundary!
$cal = new vCalendar("BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VEVENT
CREATED:20181218T052603Z
LAST-MODIFIED:20181218T052734Z
DTSTAMP:20181218T052734Z
UID:7e76efc0-b1ed-4b68-b0ed-9e34889c30bd
SUMMARY:New Event
RRULE:FREQ=MONTHLY;COUNT=5;INTERVAL=2;BYMONTHDAY=10
DTSTART;VALUE=DATE:20180410
DTEND;VALUE=DATE:20180411
TRANSP:TRANSPARENT
END:VEVENT
END:VCALENDAR");
$range = getVCalendarRange($cal, "Europe/Athens");
self::assertEquals("20180409T210000Z", (string) $range->from->UTC());
// This is correctly at a different UTC time-of-day to the original, since DST has changed
self::assertEquals("20181210T220000Z", (string) $range->until->UTC());
}
}

View File

@ -1,6 +1,6 @@
HTTP/1.1 200 OK
Date: Dow, 01 Jan 2000 00:00:00 GMT
Content-Length: 14720
Content-Length: 14764
Content-Type: text/calendar;charset=UTF-8
BEGIN:VCALENDAR
@ -11,6 +11,7 @@ BEGIN:VFREEBUSY
DTSTAMP:yyyymmddThhmmssZ
DTSTART:20060930T110000Z
DTEND:correct
FREEBUSY:20031231T230000Z/20100217T000000Z
FREEBUSY:20061001T210000Z/20061001T220000Z
FREEBUSY:20061002T210000Z/20061002T220000Z
FREEBUSY:20061003T210000Z/20061003T220000Z

View File

@ -1,6 +1,6 @@
HTTP/1.1 200 OK
Date: Dow, 01 Jan 2000 00:00:00 GMT
Content-Length: 14720
Content-Length: 14764
Content-Type: text/calendar;charset=UTF-8
BEGIN:VCALENDAR
@ -11,6 +11,7 @@ BEGIN:VFREEBUSY
DTSTAMP:yyyymmddThhmmssZ
DTSTART:20060930T110000Z
DTEND:20070630T115959Z
FREEBUSY:20031231T230000Z/20100217T000000Z
FREEBUSY:20061001T210000Z/20061001T220000Z
FREEBUSY:20061002T210000Z/20061002T220000Z
FREEBUSY:20061003T210000Z/20061003T220000Z

View File

@ -1,6 +1,6 @@
HTTP/1.1 200 OK
Date: Dow, 01 Jan 2000 00:00:00 GMT
Content-Length: 14720
Content-Length: 14764
Content-Type: text/calendar;charset=UTF-8
BEGIN:VCALENDAR
@ -11,6 +11,7 @@ BEGIN:VFREEBUSY
DTSTAMP:yyyymmddThhmmssZ
DTSTART:20060930T110000Z
DTEND:20070630T115959Z
FREEBUSY:20031231T230000Z/20100217T000000Z
FREEBUSY:20061001T210000Z/20061001T220000Z
FREEBUSY:20061002T210000Z/20061002T220000Z
FREEBUSY:20061003T210000Z/20061003T220000Z

View File

@ -14,6 +14,7 @@ BEGIN:VFREEBUSY
DTSTAMP:yyyymmddThhmmssZ
DTSTART:20081021T110000Z
DTEND:20081106T110000Z
FREEBUSY:20031231T230000Z/20100217T000000Z
FREEBUSY:20081021T180000Z/20081022T040000Z
FREEBUSY:20081022T180000Z/20081023T040000Z
FREEBUSY:20081022T184500Z/20081022T193000Z

View File

@ -1,6 +1,6 @@
HTTP/1.1 200 OK
Date: Dow, 01 Jan 2000 00:00:00 GMT
Content-Length: 14720
Content-Length: 14764
Content-Type: text/calendar;charset=UTF-8
BEGIN:VCALENDAR
@ -11,6 +11,7 @@ BEGIN:VFREEBUSY
DTSTAMP:yyyymmddThhmmssZ
DTSTART:20060930T110000Z
DTEND:20070630T115959Z
FREEBUSY:20031231T230000Z/20100217T000000Z
FREEBUSY:20061001T210000Z/20061001T220000Z
FREEBUSY:20061002T210000Z/20061002T220000Z
FREEBUSY:20061003T210000Z/20061003T220000Z

View File

@ -14,6 +14,7 @@ BEGIN:VFREEBUSY
DTSTAMP:yyyymmddThhmmssZ
DTSTART:correct
DTEND:correct
FREEBUSY:20031231T230000Z/20100217T000000Z
FREEBUSY:20060930T210000Z/20060930T220000Z
FREEBUSY:20061001T210000Z/20061001T220000Z
FREEBUSY:20061002T210000Z/20061002T220000Z

View File

@ -1,6 +1,6 @@
HTTP/1.1 200 OK
Date: Dow, 01 Jan 2000 00:00:00 GMT
Content-Length: 14720
Content-Length: 14764
Content-Type: text/calendar;charset=UTF-8
BEGIN:VCALENDAR
@ -11,6 +11,7 @@ BEGIN:VFREEBUSY
DTSTAMP:yyyymmddThhmmssZ
DTSTART:20060930T110000Z
DTEND:20070630T115959Z
FREEBUSY:20031231T230000Z/20100217T000000Z
FREEBUSY:20061001T210000Z/20061001T220000Z
FREEBUSY:20061002T210000Z/20061002T220000Z
FREEBUSY:20061003T210000Z/20061003T220000Z

View File

@ -14,6 +14,7 @@ BEGIN:VFREEBUSY
DTSTAMP:yyyymmddThhmmssZ
DTSTART:20080730T110000Z
DTEND:20080803T110000Z
FREEBUSY:20031231T230000Z/20100217T000000Z
FREEBUSY:20080730T190000Z/20080731T050000Z
FREEBUSY:20080730T194500Z/20080730T203000Z
FREEBUSY:20080730T210000Z/20080730T220000Z

View File

@ -1,4 +1,4 @@
The database is version XX currently at revision 1.3.2.
The database is version XX currently at revision 1.3.3.
No patches were applied.
Supported locales updated.
Updated view: dav_principal.sql applied.